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.
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 nullA 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.
| Dataset | One row per | What it carries |
|---|---|---|
vehicle-identity@1 | pattern | The whole identity record: make, model, model year, body, powertrain, weights, status. |
vehicle-recalls@1 | pattern and recall campaign | Every campaign that names the pattern's make, model and model year: authority, campaign numbers, defect, remedy, status. The key is the pair. |
vehicle-safety@1 | pattern | The 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
| # | Field | Type | Present | What it holds |
|---|---|---|---|---|
| 1 | pattern | string | always | The 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. |
| 2 | wmi | string | always | VIN positions 1 to 3, the world manufacturer identifier. |
| 3 | make | string | when known | The make's display name. |
| 4 | make_ref | ref | when known | The make's permanent ref, such as make:toyota. Join on this, not on the name. |
| 5 | model | string | when known | The model's display name. |
| 6 | model_ref | ref | when known | The model's permanent ref, such as model:toyota/rav4. |
| 7 | series | string | when known | The series, where one is recorded. |
| 8 | series_ref | ref | when known | Series within the model. Permanent key. Map it to your own codes. |
| 9 | model_year | integer | when known | The model year. |
| 10 | trim | string | when known | The trim, where one is recorded. |
| 11 | status | string | always | decoded or undecodable. An undecodable row is a pattern that is on record and has not been named; its value columns are empty. |
| 12 | status_reasons | string | when known | Why a pattern is undecodable: reason codes separated by ;. |
Body
| # | Field | Type | Present | What it holds |
|---|---|---|---|---|
| 13 | vehicle_type | string | when known | Passenger car, truck, motorcycle, trailer and so on. |
| 14 | vehicle_type_ref | ref | when known | Vehicle type. Permanent key, e.g. vehicle-type:passenger-car. Map it to your own codes. |
| 15 | body_style | string | when known | The body style. |
| 16 | body_style_ref | ref | when known | Body style. Permanent key. Map it to your own codes. |
| 17 | body_cab | string | when known | Cab type of a pickup or truck. Display name; empty when the registry holds no name for the ref. |
| 18 | body_cab_ref | ref | when known | Cab type of a pickup or truck. Permanent key. Map it to your own codes. |
| 19 | bed_type | string | when known | Bed type of a pickup. Display name; empty when the registry holds no name for the ref. |
| 20 | bed_type_ref | ref | when known | Bed type of a pickup. Permanent key. Map it to your own codes. |
| 21 | doors | integer | when known | The number of doors. |
Powertrain
| # | Field | Type | Present | What it holds |
|---|---|---|---|---|
| 22 | fuel_type | string | when known | The fuel type. |
| 23 | fuel_type_ref | ref | when known | Primary fuel. Permanent key. Map it to your own codes. |
| 24 | fuel_type_secondary | string | when known | Second fuel of a dual-fuel vehicle. Display name; empty when the registry holds no name for the ref. |
| 25 | fuel_type_secondary_ref | ref | when known | Second fuel of a dual-fuel vehicle. Permanent key. Map it to your own codes. |
| 26 | electrification | string | when known | The electrification level, such as hybrid or battery electric. |
| 27 | electrification_ref | ref | when known | Electrification level. Permanent key, e.g. electrification-level:bev-battery-electric-vehicle. Map it to your own codes. |
| 28 | drive_type | string | when known | The drive type. |
| 29 | drive_type_ref | ref | when known | Drive type. Permanent key. Map it to your own codes. |
| 30 | transmission | string | when known | Transmission. Display name; empty when the registry holds no name for the ref. |
| 31 | transmission_ref | ref | when known | Transmission. Permanent key. Map it to your own codes. |
Weight
| # | Field | Type | Present | What it holds |
|---|---|---|---|---|
| 32 | gvwr_class | string | when known | The gross vehicle weight rating class. |
| 33 | gvwr_class_ref | ref | when known | Gross 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. |
| 34 | wheels | integer | when known | Number of wheels. |
| 35 | axles | integer | when known | Number of axles. |
| 36 | shipping_weight | integer | when known | Shipping weight, as the record holds it. |
| 37 | gross_weight | integer | when known | Gross weight, as the record holds it. |
Trailer
| # | Field | Type | Present | What it holds |
|---|---|---|---|---|
| 38 | trailer_type | string | when known | Trailer type. Display name; empty when the registry holds no name for the ref. |
| 39 | trailer_type_ref | ref | when known | Trailer type. Permanent key. Map it to your own codes. |
| 40 | trailer_body_type | string | when known | Trailer body type. Display name; empty when the registry holds no name for the ref. |
| 41 | trailer_body_type_ref | ref | when known | Trailer body type. Permanent key. Map it to your own codes. |
Provenance
| # | Field | Type | Present | What it holds |
|---|---|---|---|---|
| 42 | source | string | when known | How 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. |
| 43 | make_source | string | when known | Which mapper wrote make: decode, catalogue, fill, miit or nhtsa-safety. |
| 44 | model_source | string | when known | Which mapper wrote model: decode, catalogue, fill, miit or nhtsa-safety. |
| 45 | series_source | string | when known | Which mapper wrote series: decode, catalogue, fill, miit or nhtsa-safety. |
| 46 | vehicle_type_source | string | when known | Which mapper wrote vehicle_type: decode, catalogue, fill, miit or nhtsa-safety. |
| 47 | body_style_source | string | when known | Which mapper wrote body_style: decode, catalogue, fill, miit or nhtsa-safety. |
| 48 | body_cab_source | string | when known | Which mapper wrote body_cab: decode, catalogue, fill, miit or nhtsa-safety. |
| 49 | bed_type_source | string | when known | Which mapper wrote bed_type: decode, catalogue, fill, miit or nhtsa-safety. |
| 50 | fuel_type_source | string | when known | Which mapper wrote fuel_type: decode, catalogue, fill, miit or nhtsa-safety. |
| 51 | fuel_type_secondary_source | string | when known | Which mapper wrote fuel_type_secondary: decode, catalogue, fill, miit or nhtsa-safety. |
| 52 | electrification_source | string | when known | Which mapper wrote electrification: decode, catalogue, fill, miit or nhtsa-safety. |
| 53 | drive_type_source | string | when known | Which mapper wrote drive_type: decode, catalogue, fill, miit or nhtsa-safety. |
| 54 | transmission_source | string | when known | Which mapper wrote transmission: decode, catalogue, fill, miit or nhtsa-safety. |
| 55 | gvwr_class_source | string | when known | Which mapper wrote gvwr_class: decode, catalogue, fill, miit or nhtsa-safety. |
| 56 | trailer_type_source | string | when known | Which mapper wrote trailer_type: decode, catalogue, fill, miit or nhtsa-safety. |
| 57 | trailer_body_type_source | string | when known | Which mapper wrote trailer_body_type: decode, catalogue, fill, miit or nhtsa-safety. |
| 58 | model_year_source | string | when known | Which mapper wrote model_year: decode, catalogue, fill, miit or nhtsa-safety. |
| 59 | trim_source | string | when known | Which mapper wrote trim: decode, catalogue, fill, miit or nhtsa-safety. |
| 60 | doors_source | string | when known | Which mapper wrote doors: decode, catalogue, fill, miit or nhtsa-safety. |
| 61 | wheels_source | string | when known | Which mapper wrote wheels: decode, catalogue, fill, miit or nhtsa-safety. |
| 62 | axles_source | string | when known | Which mapper wrote axles: decode, catalogue, fill, miit or nhtsa-safety. |
| 63 | shipping_weight_source | string | when known | Which mapper wrote shipping_weight: decode, catalogue, fill, miit or nhtsa-safety. |
| 64 | gross_weight_source | string | when known | Which mapper wrote gross_weight: decode, catalogue, fill, miit or nhtsa-safety. |
| 65 | identity_build | string | always | The identity build the row was produced from. |
| 66 | updated_at | date | always | The 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:
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 CPositions 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.
patternis 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
| Format | Files | Notes |
|---|---|---|
| CSV | full.csv, delta.csv, deletes.csv | One row per line. |
| JSON Lines | full.jsonl, delta.jsonl, deletes.jsonl | One JSON object per line. |
| JSON | full.json, delta.json, deletes.json | One JSON array. |
Each format is available plain or gzipped. A gzipped file adds .gz to its name.
CSV has three more settings:
| Setting | Choices | Default |
|---|---|---|
| Delimiter | comma, pipe, tab, semicolon | comma |
| Header line | on, off | on |
| Line ending | LF, CRLF | LF |
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:
| File | Holds |
|---|---|
| full | Every row in the feed. |
| delta | The rows that are new or changed since the current run. |
| deletes | The 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:
| Outcome | What happened | What is current afterwards |
|---|---|---|
published | The run was written and the latest pointer moved to it. | The new run. |
unchanged | The data is identical to the current run, so the run id is the same. | The same run. |
held | The run changes more rows than the limit allows. It is written and held for review. | The previous run. |
refused | The run removes too large a share of the rows. It is not published. | The previous run. |
failed | The 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.
curl "https://api.cardog.app/v2/feeds" \
-H "x-api-key: $CARDOG_ORG_KEY"Example
1 keysGET /v2/feeds lists the organization's active feeds. latest is null until a run has been published.
curl "https://api.cardog.app/v2/feeds/vehicle-identity@1/latest" \
-H "x-api-key: $CARDOG_ORG_KEY"Example
6 keysThe example is shortened: one dataset field, one artifact link and three columns are shown. Its counts and hashes are illustrative.
manifestdescribes the run.kindisdeltawhen the run was compared with an earlier one, andbaselineRunIdnames that run.kindisfullwhen there was nothing to compare with.artifactsis one entry per file, each with a signedurl.columnsis the files' columns in order: the name you chose, the dataset field behind it, and its type.layoutis how a CSV file was written.datasetis 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
curl "$ARTIFACT_URL" -o full.csv.gzOpen 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.
| Status | When |
|---|---|
400 invalid_request | The dataset id is not name@version. |
403 forbidden | The credential is not an organization key, or the artifact link is expired or not valid. |
404 not_found | The organization has no active feed for that dataset, or no run has been published yet. |
503 unavailable | The 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.
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:
| Member | Returns |
|---|---|
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.columns | The files' columns in order: name, source, type. |
run.manifest | The 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.runId | The 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.
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
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:
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.
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.
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:
mode | When | What is called |
|---|---|---|
unchanged | The current run is the one you hold. | Nothing. |
delta | The 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. |
full | Your 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
baselineRunIdnames. If that is not the run you hold,syncapplies the full file instead. It needsresetfor 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
FeedIntegrityErrorand none of your handlers is called. - Save the run id only after
syncreturns. If your process stops part way, the nextsyncwith the old id applies the same run again, and applying it twice is harmless.
To ask only whether there is anything new:
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.
# 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.runIdartifacts[].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.
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.