# Vehicles

The vehicle identification APIs: a VIN decoder, a German HSN/TSN lookup, a Danish licence plate lookup and vehicle data normalization. Every endpoint that describes a vehicle returns it the same way: `vehicle` is an [AutoUncle Vehicle Schema (AVS)](https://github.com/AutoUncle/avs) 0.3 vehicle document (which car it is, how it is built, what it is equipped with, and its registration, odometer and colour), and beside it `identification` says how that vehicle was identified against the AutoUncle catalogue: its status, the catalogue records that compete when the evidence cannot separate them, and where each value came from. Every request that describes a vehicle accepts the AVS document back as its `vehicle` (a [VehicleInput](#vehicleinput-describing-a-vehicle)), so the result of one call can be posted straight into the next.

| Endpoint | MCP tool | Starts from | Returns |
| --- | --- | --- | --- |
| [`POST /v1/vehicles/normalize`](#post-v1-vehicles-normalize) | `normalize_vehicle` | Vehicle facts, a description or photos | The [normalization result](#the-normalization-result) |
| [`POST /v1/vehicles/decode`](#post-v1-vehicles-decode) | `decode_vin` | A VIN | A [lookup result](#the-lookup-result) |
| [`POST /v1/vehicles/hsn-tsn`](#post-v1-vehicles-hsn-tsn) | `decode_hsn_tsn` | German HSN/TSN type codes | A [lookup result](#the-lookup-result) |
| [`POST /v1/vehicles/lookup`](#post-v1-vehicles-lookup) | `lookup_plate` | A registration plate (DK) | A [lookup result](#the-lookup-result) |
| [`POST /v1/valuations`](/docs/reference/valuations) | `valuate_car` | Any of the above | A [valuation result](/docs/reference/valuations#the-valuation-result) whose `vehicle` is the vehicle that was valued |

All four vehicle endpoints need the `vehicles:read` scope and share the vehicle rate limit (60 requests per minute by default). A live call that finds a vehicle costs one credit; test keys, failures and calls that find nothing are free. See [credits](/docs/credits).

> **Note:** AutoUncle Data Services is in beta. Since contract 0.9.0 the vehicle is an AVS 0.3 document; AVS itself is a draft and may still change before contract 1.0. Changes are announced in the [changelog](/docs/changelog).

## The Vehicle object

`vehicle` is a standard AVS `vehicle` document, so any AVS tooling can validate and read it. Values are plain, so you can show or store them directly, and units are in the field names (`powerHp`, `displacementCc`, `km`).

```json
{
  "vehicle": {
    "avs": { "version": "0.3", "kind": "vehicle" },
    "identity": {
      "brand": { "name": "VW", "ids": [{ "scheme": "autouncle", "value": "b-0957579dc5bca6de229d2f32" }] },
      "model": { "name": "Golf", "ids": [{ "scheme": "autouncle", "value": "cm-6bb52590daf6d1b19108ec93" }] },
      "body": { "name": "Golf VIII", "ids": [{ "scheme": "autouncle", "value": "mg-40c5d8de40bb48ae60fc0a6c" }], "type": "hatchback", "fromYear": 2019 },
      "drivetrain": { "name": "1.5 TSI (130 Hp)", "ids": [{ "scheme": "autouncle", "value": "dv-947a3199394a5a4b41cb2869" }] }
    },
    "specification": {
      "body": { "type": "hatchback", "doors": 5 },
      "powertrain": { "energy": ["petrol"], "engine": { "displacementCc": 1500 }, "powerKw": 96, "powerHp": 130,
                      "transmission": { "type": "manual" }, "traction": "2wd" }
    },
    "equipment": {
      "features": [{ "key": "INFOTAINMENT_NAVIGATION", "name": "Navigation System (GPS)", "category": "INFOTAINMENT",
                     "ids": [{ "scheme": "autouncle", "value": "f-33c7d89aab7f846afc8e2613" }], "availability": "fitted" }]
    },
    "vehicle": {
      "registration": { "country": "DK", "firstRegistrationDate": "2021-03-15", "modelYear": 2021 },
      "odometer": { "km": 45000 },
      "colour": { "name": "grey" }
    }
  },
  "identification": {
    "status": "identified",
    "level": "drivetrain",
    "catalogue_version": "5.6.9:NEW",
    "fields": {
      "/identity/drivetrain": { "basis": "catalogue", "source": "catalogue_match", "status": "derived", "match_score": 1.0 },
      "/specification/powertrain/powerHp": { "basis": "catalogue", "source": "catalogue_match", "status": "confirmed" },
      "/specification/powertrain/transmission/type": { "basis": "catalogue", "source": "catalogue_match", "status": "conflict", "supplied": "automatic" },
      "/vehicle/registration/firstRegistrationDate": { "basis": "observed", "source": "provided", "status": "supplied" }
    }
  }
}
```

The example's `fields` are shortened; a real result has an entry for every value someone stated or the catalogue derived.

An [AutoUncle Vehicle Schema (AVS)](https://github.com/AutoUncle/avs) 0.3 `vehicle` document. The [AVS specification](https://github.com/AutoUncle/avs) describes every field.

| Block | Type | Description |
| --- | --- | --- |
| `avs` **required** | object | Document metadata. Always `version` `0.3` and `kind` `vehicle`. |
| `identity` **required** | object | Where the car sits in the catalogue: brand → series → model → generation → phase → body → drivetrain, plus the configuration that pairs a body with a drivetrain. Generation, body and drivetrain are independent facts: one drivetrain is shared by every body it was sold in. Only brand and model are required; levels that are not known are left out. `brand` and `model` always; `body` (AutoUncle's generation, which includes the body, with its `type`, `fromYear` and `toYear`) and `drivetrain` when they are identified. Each level carries its AutoUncle catalogue ID as `ids[{scheme: "autouncle"}]`; an unidentified vehicle has the names as stated, without IDs. |
| `specification` | object | Technical facts about the car type. Units are always in the field name (kW, Nm, mm, kg, kWh, g/km) so there is never a unit field to look up. `body` (`type`, `doors`, `seats`, `driveSide`) and `powertrain` (`energy`, `electrification`, `engine.displacementCc`, `powerHp`, `powerKw`, `transmission.type`, `traction`). With an identified drivetrain these are the catalogue's values. |
| `equipment` | object | What the car is equipped with. A trim, package or option is identified by its NAME; that is what buyers, adverts and valuation use, and a name alone is enough to price it. Its contents are described separately as `features`. Absence is only known when stated (`fitted: false` / `availability: absent`); a missing entry means unknown. `trim`, the equipment variant (`fitted: true`), and `features[]` with `key`, `name`, `ids`, `category` and `availability`: `fitted`, or `unknown` for equipment assumed with `allow_equipment_estimation`. |
| `vehicle` | object | One physical car. Everything here can differ between two cars of the same drivetrain. `vin`, `registration` (`plate`, `country`, `firstRegistrationDate`, `modelYear`, `originalCountry`, `isImported`), `odometer` (`km`, and `readAt` for a register's reading) and `colour` (`name`, `family`). |
| `extensions` | object | Vendor-specific data that has no AVS field yet. Keys are reverse-DNS namespaces, e.g. `com.autouncle`, `de.mobile`. `com.autouncle` holds what AVS has no field for yet: `battery_kwh` (gross or usable not known), `electric_range_km` (test cycle not known), `doors` above 6, `seats` above 9, `production_year`, `first_registration` without a full date, `hsn`, `tsn`, `condition_grade` (0 to 4) and `variant_text`. |

### Reading it

- **Sparse.** Only `avs` and `identity.brand` and `identity.model` are always present. A value that is not known is left out, never sent as `null`, an empty object or an empty list. A missing equipment feature means unknown, not absent.
- **Catalogue IDs.** Each identified level carries its AutoUncle catalogue ID in `ids` as `{ "scheme": "autouncle", "value": "dv-…" }`: the same ID the [catalogue API](/docs/reference/catalogue) returns and a request accepts. A provider's own codes never appear there.
- **Generation and body.** AutoUncle's generation already includes the body (a Golf VIII hatchback and a Golf VIII estate are two generations), so it is published as AVS `identity.body`, with its `type`, `fromYear` and `toYear`. Its catalogue ID is the `mg-…` ID that `catalogue.generation_id` takes.
- **Equipment variant.** The catalogue's equipment variant is `equipment.trim`, with `fitted: true`.
- **Fuel.** `specification.powertrain.energy` lists the energy carriers and `electrification` the hybrid kind, both read from the catalogue drivetrain's own fuel. `electrification` is left out when it is not known: the catalogue's coarse plug-in bucket also holds full hybrids, so a catalogue plug-in fuel says `petrol` and `electricity` without `phev`. A plug-in you or a register stated is `phev`.
- **Units and vocabularies.** `engine.displacementCc` is in cubic centimetres (a 1.5 litre engine is `1500`), `vehicle.odometer.km` is always in kilometres (a reading in miles or Scandinavian miles is converted), `traction` is `fwd`, `rwd`, `2wd` or `awd` (`4wd` is published as `awd`), a convertible body is `cabriolet`, and a pink or turquoise colour family is `other`.
- **Doors and seats.** AVS takes 1 to 6 doors and 1 to 9 seats. A larger value, such as a 12-seat minibus, is kept in the extension instead of being cut to fit.

### Extensions

`extensions["com.autouncle"]` holds facts AutoUncle has that AVS has no field for yet. Each key is present only when the value is known.

| Key | Meaning |
| --- | --- |
| `battery_kwh` | Traction battery capacity in kWh, when it is not known whether it is gross or usable. |
| `electric_range_km` | Electric range in kilometres, when the test cycle is not known. |
| `doors`, `seats` | Doors above 6 or seats above 9. |
| `production_year` | Production year, when a source states it. Not the same as the model year. |
| `first_registration` | A first registration without a full date, such as `2021-03`. A full date is in `vehicle.registration.firstRegistrationDate`. |
| `hsn`, `tsn` | The German manufacturer and type codes you sent. |
| `condition_grade` | Condition grade from 0 through 4. |
| `variant_text` | The engine, trim or version text the vehicle was described with, as given (your `variant`, or a register's model text). It is read again when the document is posted back. |

## Identification

`identification` sits beside `vehicle` in every result that describes one. It holds what AVS has no place for: how well the catalogue identified the vehicle, the catalogue records that compete, and per-value provenance. When the vehicle cannot be identified to a brand and a model, `vehicle` is left out (it would not be an AVS document) and `identification` says why.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | identified: the catalogue drivetrain is known. partially_identified: identity stops at the brand, model or generation and nothing competes. ambiguous: several catalogue records fit at some level; that level is left out of identity and listed in candidates. unidentified: the catalogue does not know the make; the vehicle carries the brand and model names as stated, without IDs. One of `identified`, `partially_identified`, `ambiguous`, `unidentified`. |
| `level` | string | The deepest catalogue level identified. `generation` is AutoUncle's generation-with-body, published as AVS `identity.body`. One of `brand`, `model`, `generation`, `drivetrain`. |
| `catalogue_version` | string | The catalogue release that identified the vehicle, as GET /v1/catalogue/version reports it. |
| `candidates` | object | Catalogue records that fit when a level is ambiguous, at most ten per level; `fields./identity/body.candidate_count` or `fields./identity/drivetrain.candidate_count` has the total. Left out when nothing is ambiguous. |
| `candidates.generation` | [VehicleCandidate](/docs/reference/vehicles#candidates) array | Generations that fit, oldest first. At most 10 items. |
| `candidates.drivetrain` | [VehicleCandidate](/docs/reference/vehicles#candidates) array | Drivetrains of the identified generation that fit, closest to the supplied power first. At most 10 items. |
| `fields` | object | Provenance per value, keyed by JSON Pointer into `vehicle`, such as `/specification/powertrain/powerHp`, `/identity/drivetrain` or `/vehicle/odometer`. A value without provenance here was not stated by anyone; an ambiguous level appears without a value. |

### Status

| `status` | Meaning | What to do |
| --- | --- | --- |
| `identified` | The catalogue drivetrain is known, so the specification is the catalogue's. | Use it. |
| `partially_identified` | Identity stops at the brand, model or generation (`identity.body`), and nothing competes. | Add a fact that names the drivetrain (power, fuel, gearbox, a `variant`) or a catalogue ID; `improve_precision` says which. |
| `ambiguous` | Several catalogue records fit at some level. That level is left out of `vehicle.identity` and listed in `identification.candidates`. | Let the user pick a candidate and send its `id` back. Never pick one silently. |
| `unidentified` | The catalogue does not know the make. `vehicle.identity` carries the names as you or the register gave them, without catalogue IDs, and their provenance status is `unmatched`. | Check the spelling, or search the [catalogue](/docs/reference/catalogue). |

`level` is the deepest catalogue level identified: `brand`, `model`, `generation` (published as `identity.body`) or `drivetrain`.

### Field provenance

`identification.fields` is keyed by JSON Pointer into `vehicle`, such as `/identity/drivetrain`, `/specification/powertrain/powerHp`, `/vehicle/odometer` or `/equipment/features/0`. Each entry says where that value came from:

| Field | Type | Description |
| --- | --- | --- |
| `basis` | string | How the value was established: observed (supplied by you or read from a register record), extracted (parsed or automatically interpreted from your text or photos), catalogue (derived from or matched against the AutoUncle catalogue) or assumed (a default, not evidence about this vehicle). One of `observed`, `extracted`, `catalogue`, `assumed`. |
| `source` | string | What supplied it: provided (you sent it), registry (a register or decoder, named in `source_id`), catalogue_id (a catalogue ID you sent), catalogue_match (matched from names and specs), catalogue_derived (read from a catalogue record you pinned), text_parser, ai_text or ai_vision (read from your text or photos), imputed or default. One of `provided`, `registry`, `catalogue_id`, `catalogue_match`, `catalogue_derived`, `text_parser`, `ai_text`, `ai_vision`, `imputed`, `default`. |
| `source_id` | string | The `source_id` in `attributions` that supplied the value, when it came from a data source such as a register. Pattern `^[a-z][a-z0-9_]*$`. |
| `evidence_source_id` | string | The data source, also listed in attributions, that stated what the catalogue checked: for an identity level matched from a register's or decoder's names, the source of those names; for a confirmed or conflicting spec, the register that stated the value (otherwise you did). Pattern `^[a-z][a-z0-9_]*$`. |
| `status` | string | supplied: only you or a register give the value; nothing could check it. extracted: only your text or photos give the value, as the rules parser or AI interpretation read it; nothing could check it. confirmed: your value agrees with the identified drivetrain. derived: the catalogue gives it. conflict: your value contradicts the identified drivetrain; the catalogue's value is used and yours is in `supplied`. ambiguous: not determined, see `candidates`. assumed: a default, not evidence. unmatched: an identity level the catalogue could not match; its `id` is null. One of `supplied`, `extracted`, `confirmed`, `derived`, `conflict`, `ambiguous`, `assumed`, `unmatched`. |
| `supplied` | any | With status conflict: the value you sent, or the register named in `evidence_source_id` stated. It was not used. |
| `confidence` | number or null | Confidence from 0 to 1 for an interpreted value, or null. 0 to 1. |
| `match_score` | number | Catalogue match score from 0 to 1 for an identity level that was matched rather than supplied. |
| `selected_by` | string | How a catalogue record was chosen among records the catalogue could not separate: ai_text (automated interpretation of your description) or caller_evidence (the fuel, power, drive or transmission you sent). One of `ai_text`, `caller_evidence`. |
| `candidate_count` | integer | With status ambiguous: how many catalogue records fit. At most ten are listed in `candidates`. Minimum 0. |

How to read `status`:

| `status` | Meaning |
| --- | --- |
| `supplied` | Only you or a register give this value; nothing could check it. |
| `extracted` | Only your text or photos give this value, as the rules parser or AI interpretation read it; nothing could check it. |
| `confirmed` | Your value agrees with the identified drivetrain. |
| `derived` | The catalogue gives the value; you did not send it. |
| `conflict` | Your value contradicts the identified drivetrain. The catalogue's value is used and yours is in `supplied`; a `spec_conflict` warning explains it. |
| `ambiguous` | Not determined. `candidate_count` says how many records fit; `candidates` lists up to ten. |
| `assumed` | A default, not evidence about this vehicle. Show it as an assumption, never as a fact. |
| `unmatched` | An identity level the catalogue could not match. It has no catalogue ID. |

`source` says what supplied the value: `provided` (you), `registry` (a register or decoder, named by `source_id`, which is also listed in `attributions`), `catalogue_id`, `catalogue_match` or `catalogue_derived` (the catalogue), `text_parser`, `ai_text` or `ai_vision` (your text or photos), `imputed` or `default`. Identity entries also carry `match_score` for a matched level, `evidence_source_id` when the level was matched from a register's or decoder's names (that source is also in `attributions`), and `selected_by` when a record was chosen among records the catalogue could not separate (`ai_text`: from your description; `caller_evidence`: from the fuel, power, drive or transmission you sent). See [notes and data sources](/docs/notices) for `basis`.

A value with no entry was not stated by anyone. Equipment assumed with `options.allow_equipment_estimation` has `availability: "unknown"` in `vehicle.equipment.features` and `status: "assumed"` here.

### Candidates

When the generation or the drivetrain is ambiguous, `identification.candidates.generation` or `identification.candidates.drivetrain` lists up to ten catalogue records that fit what is known, closest to your power first for drivetrains. Candidates never contradict the fuel, power, drive or transmission you sent. Each item:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Catalogue ID of the record. |
| `name` | string | Catalogue name of the record. |
| `score` | number or null | The resolver's score for the tied records, or null when the candidates were not scored. |
| `from_year` | integer | Generation candidates: first production year. |
| `to_year` | integer | Generation candidates: last production year. Absent while in production. |
| `body` | string | Generation candidates: the catalogue's body type. |
| `specs` | object | Drivetrain candidates: the drivetrain's technical facts. |
| `specs.body` | string | Body type. One of `hatchback`, `estate`, `suv`, `sedan`, `mpv`, `convertible`, `coupe`. |
| `specs.doors` | integer | Number of doors. 1 to 10. |
| `specs.seats` | integer | Number of seats. 1 to 20. |
| `specs.energy` | object | Energy source. |
| `specs.energy.fuel` | string | Fuel or electrification type, from the public vocabulary of /v1/catalogue/enums. One of `petrol`, `diesel`, `electric`, `hybrid`, `plug_in_hybrid_petrol`, `plug_in_hybrid_diesel`, `lpg`, `cng`, `ethanol`, `hydrogen`. |
| `specs.power` | object | Engine or drivetrain power. Returned in both units; on input `hp` wins over `kw`. |
| `specs.power.hp` | number | Power in horsepower. 0 to 3000. |
| `specs.power.kw` | number | Power in kilowatts, rounded. On input it is converted to horsepower (1 kW = 1.35962 hp) when `hp` is absent. 0 to 2300. |
| `specs.engine_size_l` | number | Combustion engine displacement in litres, such as 1.5. 0 to 30. |
| `specs.transmission` | object | Gearbox. Absent when unknown; absent does not mean manual. |
| `specs.transmission.type` | string | Gearbox type. One of `manual`, `automatic`. |
| `specs.traction` | string | Driven wheels. `2wd` means two-wheel drive with the driven axle not recorded, which is what the catalogue knows for most cars. One of `fwd`, `rwd`, `2wd`, `awd`, `4wd`. |
| `specs.battery_kwh` | number | Traction battery capacity in kilowatt-hours. 0 to 500. |
| `specs.electric_range_km` | number | Electric driving range in kilometres. 0 to 3000. |
| `mismatches` | array | Drivetrain candidates: JSON Pointers of supplied values this candidate contradicts. Empty for the candidates listed today. |

To settle the level, send one candidate's `id` back (for example as `catalogue.drivetrain_id`, or in `identity.drivetrain.ids` of an AVS document), or a fact that separates them, such as `power_hp`; `improve_precision` names the most useful one.

## VehicleInput: describing a vehicle

`vehicle` in a [valuation](/docs/reference/valuations) or normalization request is a `VehicleInput`, in one of two forms:

- **An AVS vehicle document,** recognized by its `avs` member: any `vehicle` an endpoint returned, posted back unchanged, or one you build yourself. `identification` is not part of it; leave it out.
- **VehicleAttributes:** listing-style attributes (`make`, `model`, `fuel`, `power_hp`, `catalogue` IDs and so on), for integrations that only have listing data. See [request fields](/docs/concepts/vehicle#valuation-request-fields). The groups of the Vehicle that contract versions before 0.9.0 returned (`identity`, `specs`, `registration`, `listing`, `equipment`) are still accepted here, so stored results of an older integration keep working.

The two forms are not mixed: a `vehicle` with `avs` is read as an AVS document only. Flags and a free-text `variant` have no place in AVS, so send VehicleAttributes when you need them.

| AVS path | Attribute equivalent | Notes |
| --- | --- | --- |
| `identity.brand` (`ids` or `name`) | `catalogue.brand_id` or `make` | An `autouncle` ID wins over a name. |
| `identity.model` (`ids` or `name`) | `catalogue.model_id` or `model` | |
| `identity.body.ids` | `catalogue.generation_id` | AutoUncle's generation. |
| `identity.drivetrain.ids` | `catalogue.drivetrain_id` | Pins the specification: the catalogue wins and contradictions are reported as conflicts. |
| `equipment.trim.ids` | `catalogue.equipment_variant_id` | Read when `fitted` is not `false`. |
| `specification.body.type`, `doors`, `seats` | `body`, `doors`, `seats` | AVS body types without an AutoUncle equivalent are left out. |
| `specification.body.driveSide` | `drive_side` | |
| `specification.powertrain.energy` and `electrification` | `fuel` | The public fuel vocabulary of [`GET /v1/catalogue/enums`](/docs/reference/catalogue#get-v1-catalogue-enums). |
| `specification.powertrain.powerHp`, or `powerKw` | `power_hp` | kW is converted (1 kW = 1.35962 hp) when `powerHp` is absent. |
| `specification.powertrain.engine.displacementCc` | `engine_size_l` | |
| `specification.powertrain.transmission.type` | `transmission` | `dct`, `cvt` and `single_speed` count as automatic. |
| `specification.powertrain.traction` | `drive` | |
| `specification.battery`, `efficiency[].electricRangeKm`, or the extension | `battery_kwh`, `electric_range_km` | |
| `vehicle.registration.firstRegistrationDate`, `modelYear` | `first_registration`, `model_year` | |
| `vehicle.vin`, `vehicle.registration.plate` | `vin`, `registration_plate` | Looked up with a live key, as described in [VIN and registration plate](/docs/concepts/vehicle#vin-and-registration-plate). |
| `vehicle.registration.originalCountry` | `registration.original_market` | |
| `extensions["com.autouncle"].hsn` and `tsn` | `registration.hsn` and `tsn` | Looked up in the KBA type snapshot. |
| `vehicle.odometer.km` | `mileage` | Always kilometres. |
| `vehicle.colour.name` | `color` | |
| `extensions["com.autouncle"].condition_grade` | `condition` | |
| `extensions["com.autouncle"].variant_text` | `variant` | |
| `equipment.features[].key` | `equipment` | Only features with `availability` `fitted` (or none) are read. |

Read-only parts of a returned document are accepted and ignored: `avs.id`, catalogue names and years next to an ID, `powerKw` next to `powerHp`, `vehicle.registration.country` (the request's `country` is the market) and `vehicle.colour.family`. AVS fields the valuation does not use, such as dimensions or history, are accepted and ignored too.

In VehicleAttributes, when a value in an old canonical group and a loose attribute set the same thing differently, the canonical value wins and a `vehicle_input_overridden` warning names the loose field. A catalogue ID wins over a name at the same level.

### Posting a result back

A lookup tells you what the car is, not what it has done since. Before you value a looked-up vehicle, replace `vehicle.odometer` with today's reading, without `readAt`. A register's reading is from the last periodic inspection, dated by `readAt`; posted back unchanged it is ignored (`dated_mileage_ignored`), so the valuation is refused for missing mileage unless you send today's reading.

**curl**

```bash
curl https://data-services.autouncle.com/v1/valuations \
  -H "Authorization: Bearer $AUTOUNCLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: valuation-vehicle-001' \
  -d @- <<'JSON'
{
  "country": "DK",
  "vehicle": {
    "avs": {
      "version": "0.3",
      "kind": "vehicle"
    },
    "identity": {
      "brand": {
        "name": "VW",
        "ids": [
          {
            "scheme": "autouncle",
            "value": "b-0957579dc5bca6de229d2f32"
          }
        ]
      },
      "model": {
        "name": "Golf",
        "ids": [
          {
            "scheme": "autouncle",
            "value": "cm-6bb52590daf6d1b19108ec93"
          }
        ]
      },
      "body": {
        "name": "Golf VIII",
        "ids": [
          {
            "scheme": "autouncle",
            "value": "mg-40c5d8de40bb48ae60fc0a6c"
          }
        ],
        "type": "hatchback"
      },
      "drivetrain": {
        "name": "1.5 TSI (130 Hp)",
        "ids": [
          {
            "scheme": "autouncle",
            "value": "dv-947a3199394a5a4b41cb2869"
          }
        ]
      }
    },
    "specification": {
      "powertrain": {
        "energy": [
          "petrol"
        ],
        "powerHp": 130,
        "powerKw": 96,
        "transmission": {
          "type": "manual"
        }
      }
    },
    "vehicle": {
      "registration": {
        "country": "DK",
        "firstRegistrationDate": "2021-03-15",
        "modelYear": 2021
      },
      "odometer": {
        "km": 45000
      }
    }
  }
}
JSON
```

**Ruby**

```ruby
require "json"
require "net/http"

uri = URI("https://data-services.autouncle.com/v1/valuations")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("AUTOUNCLE_API_KEY")}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "valuation-vehicle-001"
request.body = JSON.generate({
  "country" => "DK",
  "vehicle" => {
    "avs" => {
      "version" => "0.3",
      "kind" => "vehicle"
    },
    "identity" => {
      "brand" => {
        "name" => "VW",
        "ids" => [
          {
            "scheme" => "autouncle",
            "value" => "b-0957579dc5bca6de229d2f32"
          }
        ]
      },
      "model" => {
        "name" => "Golf",
        "ids" => [
          {
            "scheme" => "autouncle",
            "value" => "cm-6bb52590daf6d1b19108ec93"
          }
        ]
      },
      "body" => {
        "name" => "Golf VIII",
        "ids" => [
          {
            "scheme" => "autouncle",
            "value" => "mg-40c5d8de40bb48ae60fc0a6c"
          }
        ],
        "type" => "hatchback"
      },
      "drivetrain" => {
        "name" => "1.5 TSI (130 Hp)",
        "ids" => [
          {
            "scheme" => "autouncle",
            "value" => "dv-947a3199394a5a4b41cb2869"
          }
        ]
      }
    },
    "specification" => {
      "powertrain" => {
        "energy" => [
          "petrol"
        ],
        "powerHp" => 130,
        "powerKw" => 96,
        "transmission" => {
          "type" => "manual"
        }
      }
    },
    "vehicle" => {
      "registration" => {
        "country" => "DK",
        "firstRegistrationDate" => "2021-03-15",
        "modelYear" => 2021
      },
      "odometer" => {
        "km" => 45000
      }
    }
  }
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.pretty_generate(JSON.parse(response.body))
```

**Python**

```python
import json
import os

import requests

response = requests.post(
    "https://data-services.autouncle.com/v1/valuations",
    headers={
        "Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}",
        "Idempotency-Key": "valuation-vehicle-001",
    },
    json={
        "country": "DK",
        "vehicle": {
            "avs": {
                "version": "0.3",
                "kind": "vehicle"
            },
            "identity": {
                "brand": {
                    "name": "VW",
                    "ids": [
                        {
                            "scheme": "autouncle",
                            "value": "b-0957579dc5bca6de229d2f32"
                        }
                    ]
                },
                "model": {
                    "name": "Golf",
                    "ids": [
                        {
                            "scheme": "autouncle",
                            "value": "cm-6bb52590daf6d1b19108ec93"
                        }
                    ]
                },
                "body": {
                    "name": "Golf VIII",
                    "ids": [
                        {
                            "scheme": "autouncle",
                            "value": "mg-40c5d8de40bb48ae60fc0a6c"
                        }
                    ],
                    "type": "hatchback"
                },
                "drivetrain": {
                    "name": "1.5 TSI (130 Hp)",
                    "ids": [
                        {
                            "scheme": "autouncle",
                            "value": "dv-947a3199394a5a4b41cb2869"
                        }
                    ]
                }
            },
            "specification": {
                "powertrain": {
                    "energy": [
                        "petrol"
                    ],
                    "powerHp": 130,
                    "powerKw": 96,
                    "transmission": {
                        "type": "manual"
                    }
                }
            },
            "vehicle": {
                "registration": {
                    "country": "DK",
                    "firstRegistrationDate": "2021-03-15",
                    "modelYear": 2021
                },
                "odometer": {
                    "km": 45000
                }
            }
        }
    },
    timeout=30,
)
print(json.dumps(response.json(), indent=2))
```

**JavaScript**

```javascript
const response = await fetch("https://data-services.autouncle.com/v1/valuations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTOUNCLE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "valuation-vehicle-001",
  },
  body: JSON.stringify({
    country: "DK",
    vehicle: {
      avs: {
        version: "0.3",
        kind: "vehicle"
      },
      identity: {
        brand: {
          name: "VW",
          ids: [
            {
              scheme: "autouncle",
              value: "b-0957579dc5bca6de229d2f32"
            }
          ]
        },
        model: {
          name: "Golf",
          ids: [
            {
              scheme: "autouncle",
              value: "cm-6bb52590daf6d1b19108ec93"
            }
          ]
        },
        body: {
          name: "Golf VIII",
          ids: [
            {
              scheme: "autouncle",
              value: "mg-40c5d8de40bb48ae60fc0a6c"
            }
          ],
          type: "hatchback"
        },
        drivetrain: {
          name: "1.5 TSI (130 Hp)",
          ids: [
            {
              scheme: "autouncle",
              value: "dv-947a3199394a5a4b41cb2869"
            }
          ]
        }
      },
      specification: {
        powertrain: {
          energy: [
            "petrol"
          ],
          powerHp: 130,
          powerKw: 96,
          transmission: {
            type: "manual"
          }
        }
      },
      vehicle: {
        registration: {
          country: "DK",
          firstRegistrationDate: "2021-03-15",
          modelYear: 2021
        },
        odometer: {
          km: 45000
        }
      }
    }
  }),
});
console.log(JSON.stringify(await response.json(), null, 2));
```

[Open this request in the console playground](https://data-services.autouncle.com/console/playground?example=valuation_vehicle)

## How a vehicle is identified

Every endpoint runs the same identification, so a vehicle identifies the same way whether you normalize it, look it up or value it:

1. **Identifiers are looked up.** A VIN, plate or HSN/TSN pair goes to the source for the market: the Danish Motor Register for plates and VINs in DK, VIN decoding elsewhere, the KBA type snapshot for HSN/TSN. Nothing is looked up in DE, IT or ES on a valuation's or normalization's behalf, and test keys never call a source.
2. **The source's answer is mapped** to the public vocabulary (fuel, body, gearbox, colour). A label without a public equivalent is left out rather than guessed. Looked-up values only fill what you left out; a value that disagrees with yours gives a `registry_conflict` warning and yours is used.
3. **The facts are matched against the catalogue:** catalogue IDs first, then names, specs, your text and, where configured, automated interpretation of text and photos. A drivetrain is chosen only when the evidence names one; otherwise identity stops at the generation, or the competing records become candidates.
4. **The vehicle is published** as an AVS document with the catalogue's values where the drivetrain is known and your or the source's values elsewhere, and `identification` gives the status, candidates and provenance for every value.

A lookup's `status` reports what the source said; `identification.status` reports how well the catalogue identified it. A register that knows the car but names a make the catalogue does not know gives `status: resolved` and `identification.status: unidentified`, with the register's names in `vehicle.identity` and its values in `specification` and `vehicle`.

## POST /v1/vehicles/normalize

Resolve a vehicle without valuing it.

Identifies a vehicle against the AutoUncle catalogue without valuing it. Send a VehicleInput (catalogue IDs, names, specs, identifiers, or a Vehicle another endpoint returned), a description or images, the same as a valuation accepts, and get back the Vehicle: catalogue identity, specs, equipment, registration and listing, with provenance per field. A VIN or plate in the vehicle is looked up as for a valuation. When several catalogue records fit, the ambiguous level is left out of identity and the records are returned as candidates. One credit per successful live normalization; refusals, failures, idempotent replays and test keys are free. Needs the vehicles:read scope and counts against the vehicle rate limit; it shares the valuation concurrency limits. Retry an unknown network outcome with the same Idempotency-Key: a replay identifies the same input again without spending a credit.

- **Scope:** `vehicles:read`
- **Cost:** One credit for a successful (`normalized`) live normalization, plus one when sent photos were used. Test keys, refusals, failures and idempotent replays are free.
- **Rate limit:** vehicles bucket, default 60 requests per minute, counted separately from valuations. See [limits](/docs/concepts/limits).

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | No | Workspace- and mode-scoped retry key. A replay of a finished normalization spends no credit. 1 to 200 characters. |

### Request body

`application/json`, a `NormalizationRequest`: A normalization request for one market. It accepts the same vehicle (a VehicleInput, including a Vehicle returned by any endpoint), description and images as a valuation request. Nothing is valued and no year or mileage is assumed. Unknown fields are rejected.

| Field | Type | Description |
| --- | --- | --- |
| `country` **required** | string | Market as an ISO 3166-1 alpha-2 code, such as DK, DE or GB. The market selects its own catalogue scope and mileage conventions. 2 to 10 characters. |
| `vehicle` | [VehicleInput](/docs/reference/vehicles#vehicleinput-describing-a-vehicle) | The vehicle, as a VehicleInput: an AVS vehicle document (any `vehicle` an endpoint returned, posted back unchanged) or the vehicle's attributes as a listing gives them. |
| `description` | string | Unstructured listing text describing the vehicle. Existing parsers and deployment-configured optional AI can propose attributes; the response identifies assumptions and unresolved inputs. 1 to 20000 characters. |
| `images` | array | Up to eight vehicle photos, each supplied by HTTPS URL or inline base64 data. AI image resolution is deployment-configured and never runs for test keys. Warnings identify unused images. On a live key, photos cost one extra credit per request (not per photo), charged only when the request succeeds and a value was read from them (`usage.charges.images`); skipped or unused photos are not charged. At most 8 items. |
| `images[].url` | string (uri) | Public HTTPS image URL on port 443, without credentials or fragments. Private network destinations and redirects are rejected. Remote images must be supported image types and at most 8 MB each. At most 2048 characters. |
| `images[].data` | string | Strict base64-encoded image bytes, without a data-URL prefix. Inline data must also fit the total 1 MB API request limit. At most 1000000 characters. |
| `images[].media_type` | string | MIME type for inline image bytes: image/jpeg, image/png, image/webp or image/gif. The actual image signature is checked. At most 100 characters. |
| `options` | object | How the vehicle is resolved. |
| `options.resolution` | string | auto uses deterministic resolution and optional configured AI; no_ai and strict use deterministic resolution only. Normalization never assumes a year or mileage. Strict resolution also ignores allow_equipment_estimation. One of `auto`, `no_ai`, `strict`. |
| `options.allow_equipment_estimation` | boolean | Explicitly allow equipment the request does not state to be assumed: the catalogue's equipment for the identified trim when the catalogue lists it, otherwise the equipment at least 70% of comparable vehicles on the market have. Assumed equipment is only added, never against equipment the request, its description or its photos state or deny, and is marked source imputed, basis assumed in identification.fields (availability unknown in vehicle.equipment), with an equipment_assumed warning and an improve_precision hint. Test keys use only the catalogue trim, never market data. Default false. Strict resolution ignores it. Default `false`. |

Structured example:

```json
{
  "country": "DK",
  "vehicle": {
    "make": "Volkswagen",
    "model": "Golf",
    "model_year": 2019,
    "fuel": "diesel",
    "transmission": "automatic"
  }
}
```

Text example:

```json
{
  "country": "DK",
  "description": "VW Golf 2.0 TDI DSG Highline, 2019, 84.000 km"
}
```

Canonical example:

```json
{
  "country": "DK",
  "vehicle": {
    "identity": {
      "brand": {
        "name": "Volkswagen"
      },
      "model": {
        "name": "Golf"
      }
    },
    "specs": {
      "energy": {
        "fuel": "diesel"
      },
      "power": {
        "kw": 110
      },
      "transmission": {
        "type": "automatic"
      }
    },
    "registration": {
      "first_registration": "2019-05-14"
    },
    "listing": {
      "mileage": {
        "value": 84000,
        "unit": "km"
      }
    }
  }
}
```

### Response

`application/json`, a `NormalizationResponse`: A normalized vehicle with its provenance, any candidates, hints, warnings, usage and the common notes and attributions. Errors return a [Problem](/docs/reference/valuations#the-problem-object) as `application/problem+json`.

| Field | Type | Description |
| --- | --- | --- |
| `request_id` | string | The normalization's request ID, shown in the console request log. Pattern `^nrm_[A-Za-z0-9]+$`. |
| `object` | string | Always `normalization`. One of `normalization`. |
| `status` | string | Always `normalized`. A vehicle that cannot be resolved returns a Problem instead. One of `normalized`. |
| `country` | string | Market as an ISO 3166-1 alpha-2 code, normalized from your request (for example `DK`). Pattern `^[A-Z]{2}$`. |
| `mode` | string | `test` for a test key or `live` for a live key. Normalization is not simulated in test mode; it is only free. One of `live`, `test`. |
| `ambiguous` | boolean | True when several catalogue records fit (identification.status is ambiguous); see identification.candidates. |
| `vehicle` | [Vehicle](/docs/reference/vehicles#the-vehicle-object) | The identified vehicle. An AVS vehicle document; left out when the vehicle could not be identified to a brand and a model, with the reason in `identification`. |
| `identification` | [VehicleIdentification](/docs/reference/vehicles#identification) | How the vehicle was identified: status, depth, candidates and the provenance of every value in `vehicle`. |
| `improve_precision` | array | Inputs that would identify the vehicle more precisely, most useful first. |
| `improve_precision[].field` **required** | string | Request field to supply, for example `vehicle.catalogue.drivetrain_id` or `vehicle.power_hp`. |
| `improve_precision[].reason` **required** | string | Machine-readable reason, for example `drivetrain_ambiguous`, `drivetrain_unresolved` or `ai_interpreted`. |
| `improve_precision[].expected_impact` **required** | string | `high`, `medium` or `low`: qualitative. One of `high`, `medium`, `low`. |
| `improve_precision[].message` **required** | string | Plain-language hint, suitable for display. |
| `improve_precision[].candidates` | array | Catalogue candidates to choose from, when the hint is about an ambiguous identity. |
| `improve_precision[].candidates[].id` **required** | string | Catalogue ID of the candidate. |
| `improve_precision[].candidates[].name` **required** | string | Catalogue name of the candidate. |
| `improve_precision[].candidates[].source` **required** | string | How the candidate was found. One of `provided`, `catalogue_id`, `catalogue_derived`, `text_parser`, `registry`, `catalogue_match`, `ai_text`, `ai_vision`, `imputed`, `default`. |
| `improve_precision[].candidates[].basis` | string | How the value was established: observed (supplied by the caller or read from a registry record), extracted (parsed or automatically interpreted from supplied text or photos), catalogue (deterministically derived from or matched against the AutoUncle catalogue) or assumed (a heuristic default or fixed baseline, not evidence about this vehicle). One of `observed`, `extracted`, `catalogue`, `assumed`. |
| `improve_precision[].candidates[].match_score` | number | Match strength from 0 to 1. |
| `improve_precision[].candidates[].ambiguous` | boolean | Whether the candidate itself is ambiguous. |
| `improve_precision[].candidates[].candidate_count` | integer | Number of fitting entries when ambiguous. Minimum 2. |
| `warnings` | array | Problems with this request's input, for example `drivetrain_ambiguous`, `generation_ambiguous`, `spec_conflict`, `model_not_recognized` or `vehicle_input_overridden`. Match on `code`. |
| `warnings[].code` **required** | string | Stable machine-readable warning code. Pattern `^[a-z][a-z0-9_]*$`. |
| `warnings[].message` **required** | string | Plain-language explanation. |
| `usage` | object | Billing for this call. |
| `usage.billable` **required** | boolean | `true` when this normalization captured a credit. |
| `usage.credits_used` **required** | integer | Credits this call consumed. Always 0 on an idempotent replay. 0 to 2. |
| `usage.original_credits_used` **required** | integer | Credits the original normalization consumed (0 to 2; 1 for the normalization, plus 1 for photos), also on a replay. 0 to 2. |
| `usage.charges` **required** | object | What `original_credits_used` paid for. Empty when nothing was charged. |
| `usage.charges.base` | integer | The normalization itself, 1 credit. Minimum 1. |
| `usage.charges.images` | integer | Image processing, 1 credit per request. Only when photos were sent and a value in `identification.fields` was read from them. Minimum 1. |
| `usage.credits_remaining` **required** | integer | Workspace credits left after this call. Minimum 0. |
| `usage.cache_hit` **required** | boolean | Reserved. Always false in this version. |
| `usage.idempotent_replay` **required** | boolean | `true` when the same Idempotency-Key and body were normalized before; the input is resolved again for free. |
| `usage.mode` **required** | string | `test` or `live`. One of `live`, `test`. |
| `notes` | [Note](/docs/notices#notes) array | Standing limitations and legal notices. Always present. |
| `attributions` | [Attribution](/docs/notices#attributions) array | Data sources that contributed to this result. Always present. |

### Response headers

| Header | Description |
| --- | --- |
| `X-Request-Id` | The HTTP request ID, on every response. Use it to correlate one HTTP exchange in proxy and application logs; it echoes a valid X-Request-Id you send. It is not the valuation ID. |
| `Retry-After` | Seconds to wait before retrying. |

### Errors

| Code | HTTP | Retryable | Meaning |
| --- | --- | --- | --- |
| [`invalid_request`](/docs/errors#invalid_request) | 400 | No | Send a request matching the documented schema. |
| [`country_required`](/docs/errors#country_required) | 400 | No | Include country, the valuation market, in the request. |
| [`unsupported_country`](/docs/errors#unsupported_country) | 400 | No | Choose a supported ISO country code. |
| [`unauthenticated`](/docs/errors#unauthenticated) | 401 | No | Supply an API key as a Bearer token. |
| [`invalid_api_key`](/docs/errors#invalid_api_key) | 401 | No | Supply a valid AutoUncle Data Services API key. |
| [`key_revoked`](/docs/errors#key_revoked) | 401 | No | This key has expired or been revoked. Create a new key. |
| [`insufficient_credits`](/docs/errors#insufficient_credits) | 402 | No | The organization has no credits available. Contact sales for a package. |
| [`key_spend_limit_reached`](/docs/errors#key_spend_limit_reached) | 402 | No | This API key has reached its monthly credit limit. |
| [`insufficient_scope`](/docs/errors#insufficient_scope) | 403 | No | Use a key with the required scope. |
| [`country_not_enabled`](/docs/errors#country_not_enabled) | 403 | No | This country is not enabled for this organization or key. |
| [`organization_pending`](/docs/errors#organization_pending) | 403 | No | This workspace is waiting for AutoUncle to approve it. |
| [`organization_suspended`](/docs/errors#organization_suspended) | 403 | No | This organization is inactive. Contact support. |
| [`unknown_catalogue_id`](/docs/errors#unknown_catalogue_id) | 404 | No | The catalogue ID is unknown. Fetch current IDs from the catalogue API. |
| [`idempotency_conflict`](/docs/errors#idempotency_conflict) | 409 | No | This idempotency key was used with different input. Choose a new key. |
| [`request_in_progress`](/docs/errors#request_in_progress) | 409 | Yes | A request with this idempotency key is still running. Retry later with the same key. |
| [`payload_too_large`](/docs/errors#payload_too_large) | 413 | No | The request exceeds the maximum supported size of 1 MB. |
| [`vehicle_not_identified`](/docs/errors#vehicle_not_identified) | 422 | No | Provide more vehicle attributes or select catalogue IDs. |
| [`catalogue_id_conflict`](/docs/errors#catalogue_id_conflict) | 422 | No | The catalogue IDs do not describe the same vehicle hierarchy. |
| [`vehicle_out_of_range`](/docs/errors#vehicle_out_of_range) | 422 | No | The vehicle is outside the supported valuation range. |
| [`images_unreadable`](/docs/errors#images_unreadable) | 422 | No | Provide readable supported images. |
| [`rate_limited`](/docs/errors#rate_limited) | 429 | Yes | The request rate limit was reached. Retry after the current minute. |
| [`test_limit_reached`](/docs/errors#test_limit_reached) | 429 | Yes | This workspace has used its fair-use allowance of test-key requests for now. |
| [`concurrency_limited`](/docs/errors#concurrency_limited) | 429 | Yes | Too many valuations are running at the same time for this organization. |
| [`rate_limit_unavailable`](/docs/errors#rate_limit_unavailable) | 503 | Yes | Request limiting is temporarily unavailable. Retry later. |
| [`engine_busy`](/docs/errors#engine_busy) | 503 | Yes | The valuation engine is at capacity. Retry after a short delay. |
| [`request_timeout`](/docs/errors#request_timeout) | 504 | Yes | The valuation request exhausted its processing time budget. |
| [`resolution_timeout`](/docs/errors#resolution_timeout) | 504 | Yes | Vehicle interpretation did not finish within its time budget. |
| [`resolution_unavailable`](/docs/errors#resolution_unavailable) | 503 | Yes | Vehicle interpretation is temporarily unavailable. |
| [`resolution_invalid_response`](/docs/errors#resolution_invalid_response) | 502 | Yes | Vehicle interpretation returned an invalid response. |
| [`image_timeout`](/docs/errors#image_timeout) | 504 | Yes | Image retrieval did not finish within its time budget. |
| [`image_unavailable`](/docs/errors#image_unavailable) | 503 | Yes | The image service is temporarily unavailable. |
| [`image_invalid_response`](/docs/errors#image_invalid_response) | 502 | Yes | The image service returned an invalid response. |
| [`internal_error`](/docs/errors#internal_error) | 500 | No | The valuation could not be completed. Contact support with the request ID. |

### Examples

**curl**

```bash
curl https://data-services.autouncle.com/v1/vehicles/normalize \
  -H "Authorization: Bearer $AUTOUNCLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: normalization-001' \
  -d @- <<'JSON'
{
  "country": "DK",
  "vehicle": {
    "make": "Volkswagen",
    "model": "Golf",
    "model_year": 2019,
    "fuel": "diesel",
    "transmission": "automatic"
  }
}
JSON
```

**Ruby**

```ruby
require "json"
require "net/http"

uri = URI("https://data-services.autouncle.com/v1/vehicles/normalize")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("AUTOUNCLE_API_KEY")}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = "normalization-001"
request.body = JSON.generate({
  "country" => "DK",
  "vehicle" => {
    "make" => "Volkswagen",
    "model" => "Golf",
    "model_year" => 2019,
    "fuel" => "diesel",
    "transmission" => "automatic"
  }
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.pretty_generate(JSON.parse(response.body))
```

**Python**

```python
import json
import os

import requests

response = requests.post(
    "https://data-services.autouncle.com/v1/vehicles/normalize",
    headers={
        "Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}",
        "Idempotency-Key": "normalization-001",
    },
    json={
        "country": "DK",
        "vehicle": {
            "make": "Volkswagen",
            "model": "Golf",
            "model_year": 2019,
            "fuel": "diesel",
            "transmission": "automatic"
        }
    },
    timeout=30,
)
print(json.dumps(response.json(), indent=2))
```

**JavaScript**

```javascript
const response = await fetch("https://data-services.autouncle.com/v1/vehicles/normalize", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTOUNCLE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "normalization-001",
  },
  body: JSON.stringify({
    country: "DK",
    vehicle: {
      make: "Volkswagen",
      model: "Golf",
      model_year: 2019,
      fuel: "diesel",
      transmission: "automatic"
    }
  }),
});
console.log(JSON.stringify(await response.json(), null, 2));
```

## The normalization result

Normalization identifies a vehicle the way a valuation would, without valuing it. It never assumes a year or mileage and nothing is scored. The body accepts the same `country`, `vehicle`, `description` and `images` as a [valuation request](/docs/reference/valuations#request-fields), `options.resolution` and `options.allow_equipment_estimation` ([assumed equipment](/docs/valuations#assumed-equipment)); it has no `pricing` and none of the valuation-only options. The response's top-level `ambiguous` is `true` exactly when `identification.status` is `ambiguous`.

```json
{
  "request_id": "nrm_3f1c0e7a9b2d4c6e8f0a1b2c3d4e5f60",
  "object": "normalization",
  "status": "normalized",
  "country": "DK",
  "mode": "test",
  "ambiguous": true,
  "vehicle": {
    "avs": { "version": "0.3", "kind": "vehicle" },
    "identity": {
      "brand": { "name": "VW", "ids": [{ "scheme": "autouncle", "value": "b-0957579dc5bca6de229d2f32" }] },
      "model": { "name": "Golf VII", "ids": [{ "scheme": "autouncle", "value": "cm-88dbede1978bc412872a6e1c" }] },
      "body": { "name": "Golf VII (5-door, facelift 2017)", "ids": [{ "scheme": "autouncle", "value": "mg-43ed101760c6bc4c4c2e44bd" }],
                "type": "hatchback", "fromYear": 2017, "toYear": 2021 }
    },
    "specification": {
      "body": { "type": "hatchback" },
      "powertrain": { "energy": ["diesel"], "transmission": { "type": "automatic" } }
    },
    "vehicle": { "registration": { "country": "DK", "modelYear": 2019 } }
  },
  "identification": {
    "status": "ambiguous",
    "level": "generation",
    "catalogue_version": "5.6.9:NEW",
    "candidates": {
      "drivetrain": [
        { "id": "dv-7cf0fd34ad595be222c8d868", "name": "1.6 TDI (115 Hp) DSG", "score": 0.999999, "mismatches": [],
          "specs": { "energy": { "fuel": "diesel" }, "power": { "hp": 115 }, "engine_size_l": 1.6, "transmission": { "type": "automatic" }, "traction": "2wd", "doors": 5 } },
        { "id": "dv-19bb09bae7bda50662f935ea", "name": "2.0 TDI (150 Hp) DSG", "score": 0.999999, "mismatches": [],
          "specs": { "energy": { "fuel": "diesel" }, "power": { "hp": 150 }, "engine_size_l": 2.0, "transmission": { "type": "automatic" }, "traction": "2wd", "doors": 5 } }
      ]
    },
    "fields": {
      "/identity/body": { "basis": "catalogue", "source": "catalogue_match", "status": "derived", "match_score": 1.0 },
      "/identity/brand": { "basis": "catalogue", "source": "catalogue_match", "status": "derived", "match_score": 1.0 },
      "/identity/drivetrain": { "basis": "catalogue", "source": "catalogue_match", "status": "ambiguous", "candidate_count": 5 },
      "/identity/model": { "basis": "catalogue", "source": "catalogue_match", "status": "derived", "match_score": 1.0 },
      "/specification/body/type": { "basis": "catalogue", "source": "catalogue_derived", "status": "derived" },
      "/specification/powertrain/energy": { "basis": "observed", "source": "provided", "status": "supplied" },
      "/specification/powertrain/transmission/type": { "basis": "observed", "source": "provided", "status": "supplied" },
      "/vehicle/registration/modelYear": { "basis": "observed", "source": "provided", "status": "supplied" }
    }
  },
  "improve_precision": [
    { "field": "vehicle.catalogue.drivetrain_id", "reason": "drivetrain_ambiguous", "expected_impact": "high",
      "message": "Several drivetrains fit equally well. Provide power_hp, battery_kwh or engine_size_l, or select the drivetrain from the catalogue." }
  ],
  "warnings": [
    { "code": "drivetrain_ambiguous", "message": "More than one drivetrain fits the supplied information. Select its catalogue ID for greater precision." }
  ],
  "usage": { "billable": false, "credits_used": 0, "original_credits_used": 0, "credits_remaining": 0, "cache_hit": false, "idempotent_replay": false, "mode": "test" },
  "notes": [],
  "attributions": [{ "source_id": "autouncle_vehicle_catalogue", "...": "see notes and data sources" }]
}
```

The example is shortened to two of the five candidates. The ambiguous drivetrain is left out of `vehicle.identity` and appears in `identification.fields` without a value.

A successful live normalization uses one credit. Test keys, refusals, failures and idempotent replays are free; test keys are not simulated, only free. Normalizations appear in the console request log with a `nrm_…` ID; they are not valuations, so `GET /v1/valuations/{id}` does not return them. Retry an unknown network outcome with the same `Idempotency-Key` and body: a replay identifies the input again without spending a credit.

## Vehicle lookups

A lookup turns an identifier into a vehicle. What the source says is identified against the catalogue like any other evidence, so the result carries catalogue IDs you can send to the [catalogue](/docs/reference/catalogue) or post back to a valuation.

- **A VIN identifies the brand and usually the model and model year.** It does not identify the exact engine or trim, so a decoded vehicle rarely gets past the model or generation. It never yields German HSN/TSN codes.
- **HSN/TSN gives brand and model names only.** The Kraftfahrt-Bundesamt type rows carry no technical data, and they are a dated snapshot that does not cover every historical type.
- **A Danish plate gives the register's record:** make, model, variant, VIN, first registration, power, fuel, gearbox, body, doors, colour and the last inspection's odometer reading. That is usually enough for a drivetrain.
- **`no_match` is an answer, not an error.** The input was valid and the source does not know it. A failed lookup returns an error (`lookup_unavailable` or `lookup_timeout`); never treat it as "not found".
- **Test keys get a fixed synthetic record** (a 2021 Volkswagen Golf) with `synthetic: true` and a `synthetic_example` note, without looking the input up. Its catalogue identification is real.
- **Nothing is stored.** AutoUncle keeps the request log entry (`lkp_…` ID, status, timing, credits) but not the VIN, plate or codes, so a lookup cannot be read back. Retrying is a new lookup.

## POST /v1/vehicles/decode

Decode a VIN into an AutoUncle vehicle.

Decodes a 17-character VIN and identifies what the decoder says against the AutoUncle catalogue. The result is the same Vehicle valuations and normalizations return, with catalogue identity, provenance per field, candidates, improvement hints, notes and source attributions; post it back as the vehicle of a valuation. One credit per live lookup that returns vehicle data (`resolved`, `suggested` or `ambiguous`); `no_match`, failures and test keys are free. A VIN never yields German HSN/TSN codes; use POST /v1/vehicles/hsn-tsn with codes from the registration document. Test keys return a fixed synthetic record without decoding; its catalogue identification is real.

- **Scope:** `vehicles:read`
- **Cost:** One credit for a live lookup that returns vehicle data (`resolved`, `suggested` or `ambiguous`). `no_match`, failures and test keys are free.
- **Rate limit:** vehicles bucket, default 60 requests per minute, counted separately from valuations. See [limits](/docs/concepts/limits).

### Request body

`application/json`, a `VinDecodeRequest`: A VIN to decode. Unknown fields are rejected.

| Field | Type | Description |
| --- | --- | --- |
| `vin` **required** | string | The 17-character VIN. Spaces and lower case are accepted; look-alike characters (O for 0, I for 1) are never repaired, and a VIN containing I, O or Q is rejected. 17 to 40 characters. |
| `country` **required** | string | Your market for this lookup, as an ISO 3166-1 alpha-2 code from GET /v1/catalogue/countries. It scopes key permissions and logging; it is never treated as the car's original market. Pattern `^[A-Za-z]{2}$`. |
| `market` | string | The car's original sales market (ISO 3166-1 alpha-2), only when you know it independently. Never derive it from your market or the registration country. Pattern `^[A-Za-z]{2}$`. |
| `model_year` | integer | The model year, only when you know it independently. A first registration or production year does not establish it. 1886 to 2100. |

Vin example:

```json
{
  "vin": "WVWZZZ1KZ5P000001",
  "country": "DE"
}
```

### Response

`application/json`, a `VehicleLookupResult`: The vehicle a lookup found, identified against the catalogue, with provenance, improvement hints, warnings, notes, attributions and billing. Errors return a [Problem](/docs/reference/valuations#the-problem-object) as `application/problem+json`.

| Field | Type | Description |
| --- | --- | --- |
| `object` | string | Always `vehicle_lookup`. |
| `request_id` | string | Lookup ID (`lkp_…`), also in the `X-Lookup-Id` header. Shown in the console request log. Pattern `^lkp_[A-Za-z0-9]+$`. |
| `lookup` | string | Which lookup ran. One of `vin`, `hsn_tsn`, `plate`. |
| `status` | string | The source's answer, separate from how well the catalogue identified the vehicle (`vehicle.status`). resolved: the source established vehicle data; suggested: only conditional answers (read the notes); ambiguous: the source holds competing answers; no_match: a valid input the source does not know (not billed); unsupported and invalid_input: the source could not answer this input (not billed). Test keys always get resolved. One of `resolved`, `suggested`, `ambiguous`, `no_match`, `unsupported`, `invalid_input`. |
| `country` | string | Your market for this lookup, normalized (ISO 3166-1 alpha-2). Pattern `^[A-Z]{2}$`. |
| `mode` | string | `test` for a test key, `live` for a live key. One of `live`, `test`. |
| `synthetic` | boolean | `true` for the fixed example a test key gets. A synthetic vehicle says nothing about your input. |
| `vehicle` | [Vehicle](/docs/reference/vehicles#the-vehicle-object) | What the source says, identified against the AutoUncle catalogue, as an AVS vehicle document: catalogue identity where the catalogue knows the make, and the source's values otherwise. Provenance and candidates are in `identification`. A make the catalogue does not know gives `identification.status: unidentified`, with the stated brand and model names and no IDs. It is left out when the vehicle could not be identified to a brand and a model, with the reason in `identification`. |
| `identification` | [VehicleIdentification](/docs/reference/vehicles#identification) | How the vehicle was identified: status, depth, candidates and the provenance of every value in `vehicle`. |
| `freshness` | [LookupFreshness](/docs/reference/vehicles#freshness) | How current the answer is. |
| `improve_precision` | array | Inputs that would identify the vehicle more precisely, most useful first. Send them with the vehicle to POST /v1/vehicles/normalize or POST /v1/valuations. |
| `improve_precision[].field` **required** | string | Request field to supply, for example `vehicle.catalogue.drivetrain_id` or `vehicle.power_hp`. |
| `improve_precision[].reason` **required** | string | Machine-readable reason, for example `drivetrain_ambiguous` or `drivetrain_unresolved`. |
| `improve_precision[].expected_impact` **required** | string | `high`, `medium` or `low`: qualitative. One of `high`, `medium`, `low`. |
| `improve_precision[].message` **required** | string | Plain-language hint, suitable for display. |
| `improve_precision[].candidates` | array | Catalogue candidates to choose from, when the hint is about an ambiguous identity. |
| `improve_precision[].candidates[].id` **required** | string | Catalogue ID of the candidate. |
| `improve_precision[].candidates[].name` **required** | string | Catalogue name of the candidate. |
| `improve_precision[].candidates[].source` **required** | string | How the candidate was found. One of `provided`, `catalogue_id`, `catalogue_derived`, `text_parser`, `registry`, `catalogue_match`, `ai_text`, `ai_vision`, `imputed`, `default`. |
| `improve_precision[].candidates[].basis` | string | How the value was established: observed (supplied by the caller or read from a registry record), extracted (parsed or automatically interpreted from supplied text or photos), catalogue (deterministically derived from or matched against the AutoUncle catalogue) or assumed (a heuristic default or fixed baseline, not evidence about this vehicle). One of `observed`, `extracted`, `catalogue`, `assumed`. |
| `improve_precision[].candidates[].match_score` | number | Match strength from 0 to 1. |
| `improve_precision[].candidates[].ambiguous` | boolean | Whether the candidate itself is ambiguous. |
| `improve_precision[].candidates[].candidate_count` | integer | Number of fitting entries when ambiguous. Minimum 2. |
| `warnings` | array | What the source or the catalogue could not use, for example `registry_no_match`, `registry_identifier_invalid`, `model_not_recognized` or `drivetrain_ambiguous`. Match on `code`. |
| `warnings[].code` **required** | string | Stable machine-readable warning code. Pattern `^[a-z][a-z0-9_]*$`. |
| `warnings[].message` **required** | string | Plain-language explanation. |
| `notes` | [Note](/docs/notices#notes) array | Standing limitations of the source, and the assumptions behind any `assumed` value. Always present. |
| `attributions` | [Attribution](/docs/notices#attributions) array | Data sources behind the published values, with their licence and attribution text. Always present; show them with the data. |
| `usage` | object | Billing for this call. |
| `usage.billable` **required** | boolean | `true` when this lookup captured a credit. |
| `usage.credits_used` **required** | integer | Credits this call consumed (0 or 1). 0 to 1. |
| `usage.original_credits_used` **required** | integer | The same as `credits_used`: lookups are never replayed. 0 to 1. |
| `usage.charges` **required** | object | What `credits_used` paid for. Empty when nothing was charged. |
| `usage.charges.base` | integer | The lookup itself, 1 credit. Minimum 1. |
| `usage.credits_remaining` **required** | integer | Workspace credits left after this call. Minimum 0. |
| `usage.cache_hit` **required** | boolean | `true` when the answer came from AutoUncle's lookup cache. Cached answers are billed like fresh ones. |
| `usage.idempotent_replay` **required** | boolean | Always false: lookups are not replayed. |
| `usage.mode` **required** | string | `test` or `live`. One of `live`, `test`. |

### Response headers

| Header | Description |
| --- | --- |
| `X-Request-Id` | The HTTP request ID, on every response. Use it to correlate one HTTP exchange in proxy and application logs; it echoes a valid X-Request-Id you send. It is not the valuation ID. |
| `X-Lookup-Id` | The vehicle lookup's ID (`lkp_…`), the same value as the body's `request_id`. Present on every lookup that was started, including failed ones. Use it in the console request log and with support. Lookup results are not stored, so it cannot be read back. |

### Errors

| Code | HTTP | Retryable | Meaning |
| --- | --- | --- | --- |
| [`invalid_request`](/docs/errors#invalid_request) | 400 | No | Send a request matching the documented schema. |
| [`unsupported_country`](/docs/errors#unsupported_country) | 400 | No | Choose a supported ISO country code. |
| [`unauthenticated`](/docs/errors#unauthenticated) | 401 | No | Supply an API key as a Bearer token. |
| [`invalid_api_key`](/docs/errors#invalid_api_key) | 401 | No | Supply a valid AutoUncle Data Services API key. |
| [`key_revoked`](/docs/errors#key_revoked) | 401 | No | This key has expired or been revoked. Create a new key. |
| [`insufficient_credits`](/docs/errors#insufficient_credits) | 402 | No | The organization has no credits available. Contact sales for a package. |
| [`key_spend_limit_reached`](/docs/errors#key_spend_limit_reached) | 402 | No | This API key has reached its monthly credit limit. |
| [`insufficient_scope`](/docs/errors#insufficient_scope) | 403 | No | Use a key with the required scope. |
| [`country_not_enabled`](/docs/errors#country_not_enabled) | 403 | No | This country is not enabled for this organization or key. |
| [`organization_pending`](/docs/errors#organization_pending) | 403 | No | This workspace is waiting for AutoUncle to approve it. |
| [`organization_suspended`](/docs/errors#organization_suspended) | 403 | No | This organization is inactive. Contact support. |
| [`payload_too_large`](/docs/errors#payload_too_large) | 413 | No | The request exceeds the maximum supported size of 1 MB. |
| [`rate_limited`](/docs/errors#rate_limited) | 429 | Yes | The request rate limit was reached. Retry after the current minute. |
| [`test_limit_reached`](/docs/errors#test_limit_reached) | 429 | Yes | This workspace has used its fair-use allowance of test-key requests for now. |
| [`rate_limit_unavailable`](/docs/errors#rate_limit_unavailable) | 503 | Yes | Request limiting is temporarily unavailable. Retry later. |
| [`lookup_unavailable`](/docs/errors#lookup_unavailable) | 503 | Yes | The vehicle lookup service is temporarily unavailable. |
| [`lookup_timeout`](/docs/errors#lookup_timeout) | 504 | Yes | The vehicle lookup service did not respond within its time budget. |
| [`internal_error`](/docs/errors#internal_error) | 500 | No | The valuation could not be completed. Contact support with the request ID. |

### Examples

**curl**

```bash
curl https://data-services.autouncle.com/v1/vehicles/decode \
  -H "Authorization: Bearer $AUTOUNCLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<'JSON'
{
  "vin": "WVWZZZ1KZ5P000001",
  "country": "DE"
}
JSON
```

**Ruby**

```ruby
require "json"
require "net/http"

uri = URI("https://data-services.autouncle.com/v1/vehicles/decode")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("AUTOUNCLE_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
  "vin" => "WVWZZZ1KZ5P000001",
  "country" => "DE"
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.pretty_generate(JSON.parse(response.body))
```

**Python**

```python
import json
import os

import requests

response = requests.post(
    "https://data-services.autouncle.com/v1/vehicles/decode",
    headers={
        "Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}",
    },
    json={
        "vin": "WVWZZZ1KZ5P000001",
        "country": "DE"
    },
    timeout=30,
)
print(json.dumps(response.json(), indent=2))
```

**JavaScript**

```javascript
const response = await fetch("https://data-services.autouncle.com/v1/vehicles/decode", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTOUNCLE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    vin: "WVWZZZ1KZ5P000001",
    country: "DE"
  }),
});
console.log(JSON.stringify(await response.json(), null, 2));
```

### Example VIN response

```json
{
  "object": "vehicle_lookup",
  "request_id": "lkp_6f1d0c2e9a8b47d3b5e1c0a9f2d4e6b8",
  "lookup": "vin",
  "status": "resolved",
  "country": "DE",
  "mode": "live",
  "synthetic": false,
  "vehicle": {
    "avs": { "version": "0.3", "kind": "vehicle" },
    "identity": {
      "brand": { "name": "VW", "ids": [{ "scheme": "autouncle", "value": "b-0957579dc5bca6de229d2f32" }] },
      "model": { "name": "Golf VII", "ids": [{ "scheme": "autouncle", "value": "cm-88dbede1978bc412872a6e1c" }] }
    },
    "vehicle": {
      "vin": "WVWZZZAUZKW000001",
      "registration": { "country": "DE", "modelYear": 2019 }
    }
  },
  "identification": {
    "status": "partially_identified",
    "level": "model",
    "catalogue_version": "5.6.9:NEW",
    "fields": {
      "/identity/brand": { "basis": "catalogue", "source": "catalogue_match", "evidence_source_id": "vin_decoding", "status": "derived", "match_score": 1.0 },
      "/identity/model": { "basis": "catalogue", "source": "catalogue_match", "evidence_source_id": "vin_decoding", "status": "derived", "match_score": 1.0 },
      "/vehicle/registration/modelYear": { "basis": "observed", "source": "registry", "source_id": "vin_decoding", "status": "supplied" },
      "/vehicle/vin": { "basis": "observed", "source": "provided", "status": "supplied" }
    }
  },
  "freshness": { "retrieved_at": "2026-11-01T10:00:00Z", "cached": false, "data_as_of": null, "dataset_version": "orvin 0.3.1 / dataset 2026.09.25.2" },
  "improve_precision": [
    { "field": "vehicle.catalogue.drivetrain_id", "reason": "drivetrain_unresolved", "expected_impact": "high",
      "message": "Select the exact engine and transmission from the catalogue." }
  ],
  "warnings": [],
  "notes": [],
  "attributions": [
    { "source_id": "vin_decoding", "name": "VIN decoding", "...": "licence and attribution text" },
    { "source_id": "autouncle_vehicle_catalogue", "...": "see notes and data sources" }
  ],
  "usage": { "billable": true, "credits_used": 1, "original_credits_used": 1, "credits_remaining": 29, "cache_hit": false, "idempotent_replay": false, "mode": "live" }
}
```

The example's hint and attributions are shortened; a real response lists every source behind a published value.

## POST /v1/vehicles/hsn-tsn

Look up German HSN/TSN type codes.

Looks up the German manufacturer and type codes from a Zulassungsbescheinigung (HSN, field 2.1, and the first three characters of TSN, field 2.2) in the Kraftfahrt-Bundesamt type snapshot and identifies the brand and model names it gives against the AutoUncle catalogue. KBA type rows carry no technical data, so the vehicle rarely gets past the model. One credit per live lookup that returns vehicle data; `no_match`, failures and test keys are free.

- **Scope:** `vehicles:read`
- **Cost:** One credit for a live lookup that returns vehicle data (`resolved`, `suggested` or `ambiguous`). `no_match`, failures and test keys are free.
- **Rate limit:** vehicles bucket, default 60 requests per minute, counted separately from valuations. See [limits](/docs/concepts/limits).

### Request body

`application/json`, a `HsnTsnRequest`: German type codes to look up. Unknown fields are rejected.

| Field | Type | Description |
| --- | --- | --- |
| `hsn` **required** | string | Herstellerschlüsselnummer, field 2.1: four digits, sent as a string so leading zeroes are kept. 4 to 8 characters. |
| `tsn` **required** | string | The first three characters of the Typschlüsselnummer, field 2.2. Longer codes are rejected rather than truncated. 3 to 8 characters. |
| `country` | string | Your market for this lookup (ISO 3166-1 alpha-2). Defaults to DE. The codes are always KBA codes. Pattern `^[A-Za-z]{2}$`. |

Hsn tsn example:

```json
{
  "hsn": "0603",
  "tsn": "BMT"
}
```

### Response

`application/json`, a `VehicleLookupResult`: The vehicle a lookup found, identified against the catalogue, with provenance, improvement hints, warnings, notes, attributions and billing. Errors return a [Problem](/docs/reference/valuations#the-problem-object) as `application/problem+json`.

| Field | Type | Description |
| --- | --- | --- |
| `object` | string | Always `vehicle_lookup`. |
| `request_id` | string | Lookup ID (`lkp_…`), also in the `X-Lookup-Id` header. Shown in the console request log. Pattern `^lkp_[A-Za-z0-9]+$`. |
| `lookup` | string | Which lookup ran. One of `vin`, `hsn_tsn`, `plate`. |
| `status` | string | The source's answer, separate from how well the catalogue identified the vehicle (`vehicle.status`). resolved: the source established vehicle data; suggested: only conditional answers (read the notes); ambiguous: the source holds competing answers; no_match: a valid input the source does not know (not billed); unsupported and invalid_input: the source could not answer this input (not billed). Test keys always get resolved. One of `resolved`, `suggested`, `ambiguous`, `no_match`, `unsupported`, `invalid_input`. |
| `country` | string | Your market for this lookup, normalized (ISO 3166-1 alpha-2). Pattern `^[A-Z]{2}$`. |
| `mode` | string | `test` for a test key, `live` for a live key. One of `live`, `test`. |
| `synthetic` | boolean | `true` for the fixed example a test key gets. A synthetic vehicle says nothing about your input. |
| `vehicle` | [Vehicle](/docs/reference/vehicles#the-vehicle-object) | What the source says, identified against the AutoUncle catalogue, as an AVS vehicle document: catalogue identity where the catalogue knows the make, and the source's values otherwise. Provenance and candidates are in `identification`. A make the catalogue does not know gives `identification.status: unidentified`, with the stated brand and model names and no IDs. It is left out when the vehicle could not be identified to a brand and a model, with the reason in `identification`. |
| `identification` | [VehicleIdentification](/docs/reference/vehicles#identification) | How the vehicle was identified: status, depth, candidates and the provenance of every value in `vehicle`. |
| `freshness` | [LookupFreshness](/docs/reference/vehicles#freshness) | How current the answer is. |
| `improve_precision` | array | Inputs that would identify the vehicle more precisely, most useful first. Send them with the vehicle to POST /v1/vehicles/normalize or POST /v1/valuations. |
| `improve_precision[].field` **required** | string | Request field to supply, for example `vehicle.catalogue.drivetrain_id` or `vehicle.power_hp`. |
| `improve_precision[].reason` **required** | string | Machine-readable reason, for example `drivetrain_ambiguous` or `drivetrain_unresolved`. |
| `improve_precision[].expected_impact` **required** | string | `high`, `medium` or `low`: qualitative. One of `high`, `medium`, `low`. |
| `improve_precision[].message` **required** | string | Plain-language hint, suitable for display. |
| `improve_precision[].candidates` | array | Catalogue candidates to choose from, when the hint is about an ambiguous identity. |
| `improve_precision[].candidates[].id` **required** | string | Catalogue ID of the candidate. |
| `improve_precision[].candidates[].name` **required** | string | Catalogue name of the candidate. |
| `improve_precision[].candidates[].source` **required** | string | How the candidate was found. One of `provided`, `catalogue_id`, `catalogue_derived`, `text_parser`, `registry`, `catalogue_match`, `ai_text`, `ai_vision`, `imputed`, `default`. |
| `improve_precision[].candidates[].basis` | string | How the value was established: observed (supplied by the caller or read from a registry record), extracted (parsed or automatically interpreted from supplied text or photos), catalogue (deterministically derived from or matched against the AutoUncle catalogue) or assumed (a heuristic default or fixed baseline, not evidence about this vehicle). One of `observed`, `extracted`, `catalogue`, `assumed`. |
| `improve_precision[].candidates[].match_score` | number | Match strength from 0 to 1. |
| `improve_precision[].candidates[].ambiguous` | boolean | Whether the candidate itself is ambiguous. |
| `improve_precision[].candidates[].candidate_count` | integer | Number of fitting entries when ambiguous. Minimum 2. |
| `warnings` | array | What the source or the catalogue could not use, for example `registry_no_match`, `registry_identifier_invalid`, `model_not_recognized` or `drivetrain_ambiguous`. Match on `code`. |
| `warnings[].code` **required** | string | Stable machine-readable warning code. Pattern `^[a-z][a-z0-9_]*$`. |
| `warnings[].message` **required** | string | Plain-language explanation. |
| `notes` | [Note](/docs/notices#notes) array | Standing limitations of the source, and the assumptions behind any `assumed` value. Always present. |
| `attributions` | [Attribution](/docs/notices#attributions) array | Data sources behind the published values, with their licence and attribution text. Always present; show them with the data. |
| `usage` | object | Billing for this call. |
| `usage.billable` **required** | boolean | `true` when this lookup captured a credit. |
| `usage.credits_used` **required** | integer | Credits this call consumed (0 or 1). 0 to 1. |
| `usage.original_credits_used` **required** | integer | The same as `credits_used`: lookups are never replayed. 0 to 1. |
| `usage.charges` **required** | object | What `credits_used` paid for. Empty when nothing was charged. |
| `usage.charges.base` | integer | The lookup itself, 1 credit. Minimum 1. |
| `usage.credits_remaining` **required** | integer | Workspace credits left after this call. Minimum 0. |
| `usage.cache_hit` **required** | boolean | `true` when the answer came from AutoUncle's lookup cache. Cached answers are billed like fresh ones. |
| `usage.idempotent_replay` **required** | boolean | Always false: lookups are not replayed. |
| `usage.mode` **required** | string | `test` or `live`. One of `live`, `test`. |

### Response headers

| Header | Description |
| --- | --- |
| `X-Request-Id` | The HTTP request ID, on every response. Use it to correlate one HTTP exchange in proxy and application logs; it echoes a valid X-Request-Id you send. It is not the valuation ID. |
| `X-Lookup-Id` | The vehicle lookup's ID (`lkp_…`), the same value as the body's `request_id`. Present on every lookup that was started, including failed ones. Use it in the console request log and with support. Lookup results are not stored, so it cannot be read back. |

### Errors

| Code | HTTP | Retryable | Meaning |
| --- | --- | --- | --- |
| [`invalid_request`](/docs/errors#invalid_request) | 400 | No | Send a request matching the documented schema. |
| [`unsupported_country`](/docs/errors#unsupported_country) | 400 | No | Choose a supported ISO country code. |
| [`unauthenticated`](/docs/errors#unauthenticated) | 401 | No | Supply an API key as a Bearer token. |
| [`invalid_api_key`](/docs/errors#invalid_api_key) | 401 | No | Supply a valid AutoUncle Data Services API key. |
| [`key_revoked`](/docs/errors#key_revoked) | 401 | No | This key has expired or been revoked. Create a new key. |
| [`insufficient_credits`](/docs/errors#insufficient_credits) | 402 | No | The organization has no credits available. Contact sales for a package. |
| [`key_spend_limit_reached`](/docs/errors#key_spend_limit_reached) | 402 | No | This API key has reached its monthly credit limit. |
| [`insufficient_scope`](/docs/errors#insufficient_scope) | 403 | No | Use a key with the required scope. |
| [`country_not_enabled`](/docs/errors#country_not_enabled) | 403 | No | This country is not enabled for this organization or key. |
| [`organization_pending`](/docs/errors#organization_pending) | 403 | No | This workspace is waiting for AutoUncle to approve it. |
| [`organization_suspended`](/docs/errors#organization_suspended) | 403 | No | This organization is inactive. Contact support. |
| [`payload_too_large`](/docs/errors#payload_too_large) | 413 | No | The request exceeds the maximum supported size of 1 MB. |
| [`rate_limited`](/docs/errors#rate_limited) | 429 | Yes | The request rate limit was reached. Retry after the current minute. |
| [`test_limit_reached`](/docs/errors#test_limit_reached) | 429 | Yes | This workspace has used its fair-use allowance of test-key requests for now. |
| [`rate_limit_unavailable`](/docs/errors#rate_limit_unavailable) | 503 | Yes | Request limiting is temporarily unavailable. Retry later. |
| [`lookup_unavailable`](/docs/errors#lookup_unavailable) | 503 | Yes | The vehicle lookup service is temporarily unavailable. |
| [`lookup_timeout`](/docs/errors#lookup_timeout) | 504 | Yes | The vehicle lookup service did not respond within its time budget. |
| [`internal_error`](/docs/errors#internal_error) | 500 | No | The valuation could not be completed. Contact support with the request ID. |

### Examples

**curl**

```bash
curl https://data-services.autouncle.com/v1/vehicles/hsn-tsn \
  -H "Authorization: Bearer $AUTOUNCLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<'JSON'
{
  "hsn": "0603",
  "tsn": "BMT"
}
JSON
```

**Ruby**

```ruby
require "json"
require "net/http"

uri = URI("https://data-services.autouncle.com/v1/vehicles/hsn-tsn")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("AUTOUNCLE_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
  "hsn" => "0603",
  "tsn" => "BMT"
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.pretty_generate(JSON.parse(response.body))
```

**Python**

```python
import json
import os

import requests

response = requests.post(
    "https://data-services.autouncle.com/v1/vehicles/hsn-tsn",
    headers={
        "Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}",
    },
    json={
        "hsn": "0603",
        "tsn": "BMT"
    },
    timeout=30,
)
print(json.dumps(response.json(), indent=2))
```

**JavaScript**

```javascript
const response = await fetch("https://data-services.autouncle.com/v1/vehicles/hsn-tsn", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTOUNCLE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    hsn: "0603",
    tsn: "BMT"
  }),
});
console.log(JSON.stringify(await response.json(), null, 2));
```

### Example HSN/TSN response

```json
{
  "object": "vehicle_lookup",
  "lookup": "hsn_tsn",
  "status": "resolved",
  "vehicle": {
    "avs": { "version": "0.3", "kind": "vehicle" },
    "identity": {
      "brand": { "name": "VW", "ids": [{ "scheme": "autouncle", "value": "b-0957579dc5bca6de229d2f32" }] },
      "model": { "name": "Golf", "ids": [{ "scheme": "autouncle", "value": "cm-6bb52590daf6d1b19108ec93" }] }
    },
    "vehicle": { "registration": { "country": "DE" } },
    "extensions": { "com.autouncle": { "hsn": "0603", "tsn": "BMT" } }
  },
  "identification": {
    "status": "partially_identified",
    "level": "model",
    "catalogue_version": "5.6.9:NEW",
    "fields": {
      "/extensions/com.autouncle/hsn": { "basis": "observed", "source": "provided", "status": "supplied" },
      "/extensions/com.autouncle/tsn": { "basis": "observed", "source": "provided", "status": "supplied" },
      "/identity/brand": { "basis": "catalogue", "source": "catalogue_match", "evidence_source_id": "de_type_approval_codes", "status": "derived", "match_score": 1.0 },
      "/identity/model": { "basis": "catalogue", "source": "catalogue_match", "evidence_source_id": "de_type_approval_codes", "status": "derived", "match_score": 0.92 }
    }
  }
}
```

The example shows only the vehicle and its identification; the envelope is the same as for a VIN. Its attributions list `de_type_approval_codes`, the Kraftfahrt-Bundesamt vehicle type data (HSN/TSN), published under the Data licence Germany – attribution – version 2.0 (`dl-de/by-2-0`): show its attribution text with the data.

## POST /v1/vehicles/lookup

Look up a vehicle by registration plate.

Looks the plate up in the country's vehicle register and identifies the registered vehicle against the AutoUncle catalogue. Denmark (DK) only in this version, through the Danish Motor Register (DMR); other countries return lookup_country_unsupported before anything is reserved. One credit per live lookup that finds the vehicle; no_match, failures and test-key lookups are free. Send the plate only in the JSON body. The plate and the register's answer are never stored; the request log keeps the outcome only.

- **Scope:** `vehicles:read`
- **Cost:** One credit for a live lookup that finds the vehicle. `no_match`, failures, unsupported countries and test keys are free.
- **Rate limit:** vehicles bucket, default 60 requests per minute, counted separately from valuations. See [limits](/docs/concepts/limits).

### Request body

`application/json`, a `PlateLookupRequest`: A registration-plate lookup. Unknown fields are rejected.

| Field | Type | Description |
| --- | --- | --- |
| `country` **required** | string | Country of the plate, ISO 3166-1 alpha-2. Only DK is supported in this version. 2 to 2 characters. |
| `plate` **required** | string | Registration plate as printed. Spaces, dashes and dots are ignored and letters are matched case-insensitively. Danish plates have 2 to 7 letters and digits, including personal plates. 1 to 20 characters. |

### Response

`application/json`, a `VehicleLookupResult`: The vehicle a lookup found, identified against the catalogue, with provenance, improvement hints, warnings, notes, attributions and billing. Errors return a [Problem](/docs/reference/valuations#the-problem-object) as `application/problem+json`.

| Field | Type | Description |
| --- | --- | --- |
| `object` | string | Always `vehicle_lookup`. |
| `request_id` | string | Lookup ID (`lkp_…`), also in the `X-Lookup-Id` header. Shown in the console request log. Pattern `^lkp_[A-Za-z0-9]+$`. |
| `lookup` | string | Which lookup ran. One of `vin`, `hsn_tsn`, `plate`. |
| `status` | string | The source's answer, separate from how well the catalogue identified the vehicle (`vehicle.status`). resolved: the source established vehicle data; suggested: only conditional answers (read the notes); ambiguous: the source holds competing answers; no_match: a valid input the source does not know (not billed); unsupported and invalid_input: the source could not answer this input (not billed). Test keys always get resolved. One of `resolved`, `suggested`, `ambiguous`, `no_match`, `unsupported`, `invalid_input`. |
| `country` | string | Your market for this lookup, normalized (ISO 3166-1 alpha-2). Pattern `^[A-Z]{2}$`. |
| `mode` | string | `test` for a test key, `live` for a live key. One of `live`, `test`. |
| `synthetic` | boolean | `true` for the fixed example a test key gets. A synthetic vehicle says nothing about your input. |
| `vehicle` | [Vehicle](/docs/reference/vehicles#the-vehicle-object) | What the source says, identified against the AutoUncle catalogue, as an AVS vehicle document: catalogue identity where the catalogue knows the make, and the source's values otherwise. Provenance and candidates are in `identification`. A make the catalogue does not know gives `identification.status: unidentified`, with the stated brand and model names and no IDs. It is left out when the vehicle could not be identified to a brand and a model, with the reason in `identification`. |
| `identification` | [VehicleIdentification](/docs/reference/vehicles#identification) | How the vehicle was identified: status, depth, candidates and the provenance of every value in `vehicle`. |
| `freshness` | [LookupFreshness](/docs/reference/vehicles#freshness) | How current the answer is. |
| `improve_precision` | array | Inputs that would identify the vehicle more precisely, most useful first. Send them with the vehicle to POST /v1/vehicles/normalize or POST /v1/valuations. |
| `improve_precision[].field` **required** | string | Request field to supply, for example `vehicle.catalogue.drivetrain_id` or `vehicle.power_hp`. |
| `improve_precision[].reason` **required** | string | Machine-readable reason, for example `drivetrain_ambiguous` or `drivetrain_unresolved`. |
| `improve_precision[].expected_impact` **required** | string | `high`, `medium` or `low`: qualitative. One of `high`, `medium`, `low`. |
| `improve_precision[].message` **required** | string | Plain-language hint, suitable for display. |
| `improve_precision[].candidates` | array | Catalogue candidates to choose from, when the hint is about an ambiguous identity. |
| `improve_precision[].candidates[].id` **required** | string | Catalogue ID of the candidate. |
| `improve_precision[].candidates[].name` **required** | string | Catalogue name of the candidate. |
| `improve_precision[].candidates[].source` **required** | string | How the candidate was found. One of `provided`, `catalogue_id`, `catalogue_derived`, `text_parser`, `registry`, `catalogue_match`, `ai_text`, `ai_vision`, `imputed`, `default`. |
| `improve_precision[].candidates[].basis` | string | How the value was established: observed (supplied by the caller or read from a registry record), extracted (parsed or automatically interpreted from supplied text or photos), catalogue (deterministically derived from or matched against the AutoUncle catalogue) or assumed (a heuristic default or fixed baseline, not evidence about this vehicle). One of `observed`, `extracted`, `catalogue`, `assumed`. |
| `improve_precision[].candidates[].match_score` | number | Match strength from 0 to 1. |
| `improve_precision[].candidates[].ambiguous` | boolean | Whether the candidate itself is ambiguous. |
| `improve_precision[].candidates[].candidate_count` | integer | Number of fitting entries when ambiguous. Minimum 2. |
| `warnings` | array | What the source or the catalogue could not use, for example `registry_no_match`, `registry_identifier_invalid`, `model_not_recognized` or `drivetrain_ambiguous`. Match on `code`. |
| `warnings[].code` **required** | string | Stable machine-readable warning code. Pattern `^[a-z][a-z0-9_]*$`. |
| `warnings[].message` **required** | string | Plain-language explanation. |
| `notes` | [Note](/docs/notices#notes) array | Standing limitations of the source, and the assumptions behind any `assumed` value. Always present. |
| `attributions` | [Attribution](/docs/notices#attributions) array | Data sources behind the published values, with their licence and attribution text. Always present; show them with the data. |
| `usage` | object | Billing for this call. |
| `usage.billable` **required** | boolean | `true` when this lookup captured a credit. |
| `usage.credits_used` **required** | integer | Credits this call consumed (0 or 1). 0 to 1. |
| `usage.original_credits_used` **required** | integer | The same as `credits_used`: lookups are never replayed. 0 to 1. |
| `usage.charges` **required** | object | What `credits_used` paid for. Empty when nothing was charged. |
| `usage.charges.base` | integer | The lookup itself, 1 credit. Minimum 1. |
| `usage.credits_remaining` **required** | integer | Workspace credits left after this call. Minimum 0. |
| `usage.cache_hit` **required** | boolean | `true` when the answer came from AutoUncle's lookup cache. Cached answers are billed like fresh ones. |
| `usage.idempotent_replay` **required** | boolean | Always false: lookups are not replayed. |
| `usage.mode` **required** | string | `test` or `live`. One of `live`, `test`. |

### Response headers

| Header | Description |
| --- | --- |
| `X-Request-Id` | The HTTP request ID, on every response. Use it to correlate one HTTP exchange in proxy and application logs; it echoes a valid X-Request-Id you send. It is not the valuation ID. |
| `X-Lookup-Id` | The vehicle lookup's ID (`lkp_…`), the same value as the body's `request_id`. Present on every lookup that was started, including failed ones. Use it in the console request log and with support. Lookup results are not stored, so it cannot be read back. |

### Errors

| Code | HTTP | Retryable | Meaning |
| --- | --- | --- | --- |
| [`invalid_request`](/docs/errors#invalid_request) | 400 | No | Send a request matching the documented schema. |
| [`unsupported_country`](/docs/errors#unsupported_country) | 400 | No | Choose a supported ISO country code. |
| [`lookup_country_unsupported`](/docs/errors#lookup_country_unsupported) | 422 | No | Registration-plate lookups are not available for this country. |
| [`unauthenticated`](/docs/errors#unauthenticated) | 401 | No | Supply an API key as a Bearer token. |
| [`invalid_api_key`](/docs/errors#invalid_api_key) | 401 | No | Supply a valid AutoUncle Data Services API key. |
| [`key_revoked`](/docs/errors#key_revoked) | 401 | No | This key has expired or been revoked. Create a new key. |
| [`insufficient_credits`](/docs/errors#insufficient_credits) | 402 | No | The organization has no credits available. Contact sales for a package. |
| [`key_spend_limit_reached`](/docs/errors#key_spend_limit_reached) | 402 | No | This API key has reached its monthly credit limit. |
| [`insufficient_scope`](/docs/errors#insufficient_scope) | 403 | No | Use a key with the required scope. |
| [`country_not_enabled`](/docs/errors#country_not_enabled) | 403 | No | This country is not enabled for this organization or key. |
| [`organization_pending`](/docs/errors#organization_pending) | 403 | No | This workspace is waiting for AutoUncle to approve it. |
| [`organization_suspended`](/docs/errors#organization_suspended) | 403 | No | This organization is inactive. Contact support. |
| [`payload_too_large`](/docs/errors#payload_too_large) | 413 | No | The request exceeds the maximum supported size of 1 MB. |
| [`rate_limited`](/docs/errors#rate_limited) | 429 | Yes | The request rate limit was reached. Retry after the current minute. |
| [`test_limit_reached`](/docs/errors#test_limit_reached) | 429 | Yes | This workspace has used its fair-use allowance of test-key requests for now. |
| [`rate_limit_unavailable`](/docs/errors#rate_limit_unavailable) | 503 | Yes | Request limiting is temporarily unavailable. Retry later. |
| [`lookup_unavailable`](/docs/errors#lookup_unavailable) | 503 | Yes | The vehicle lookup service is temporarily unavailable. |
| [`lookup_timeout`](/docs/errors#lookup_timeout) | 504 | Yes | The vehicle lookup service did not respond within its time budget. |
| [`internal_error`](/docs/errors#internal_error) | 500 | No | The valuation could not be completed. Contact support with the request ID. |

### Examples

**curl**

```bash
curl https://data-services.autouncle.com/v1/vehicles/lookup \
  -H "Authorization: Bearer $AUTOUNCLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<'JSON'
{
  "country": "DK",
  "plate": "AB12345"
}
JSON
```

**Ruby**

```ruby
require "json"
require "net/http"

uri = URI("https://data-services.autouncle.com/v1/vehicles/lookup")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("AUTOUNCLE_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = JSON.generate({
  "country" => "DK",
  "plate" => "AB12345"
})
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts JSON.pretty_generate(JSON.parse(response.body))
```

**Python**

```python
import json
import os

import requests

response = requests.post(
    "https://data-services.autouncle.com/v1/vehicles/lookup",
    headers={
        "Authorization": f"Bearer {os.environ['AUTOUNCLE_API_KEY']}",
    },
    json={
        "country": "DK",
        "plate": "AB12345"
    },
    timeout=30,
)
print(json.dumps(response.json(), indent=2))
```

**JavaScript**

```javascript
const response = await fetch("https://data-services.autouncle.com/v1/vehicles/lookup", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTOUNCLE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    country: "DK",
    plate: "AB12345"
  }),
});
console.log(JSON.stringify(await response.json(), null, 2));
```

### Plates and privacy

Denmark (`DK`) is the only country in this version, through the Danish Motor Register (DMR). Other countries return [`lookup_country_unsupported`](/docs/errors#lookup_country_unsupported) before anything is reserved, with `reason` `no_registry` when no register is connected yet, or `excluded_by_policy` where registry lookups are not offered.

- Send the plate only in the JSON body, never in a URL or query string.
- The plate and the register's answer are not stored. The request log keeps the `lkp_…` ID, market, outcome and credits.
- The register's answer never includes owner, insurance, lien or tax data.
- Register labels with a public equivalent (fuel, body, gearbox, colour) are mapped; a label without one is left out rather than guessed.
- `vehicle.vehicle.odometer` is the odometer reading at the last periodic inspection, dated by `readAt`. It is not today's mileage.
- Look up plates only for a purpose you can justify, such as a vehicle a customer asked you to check.

### Example plate response

```json
{
  "object": "vehicle_lookup",
  "request_id": "lkp_0b5e8f2a4c6d4e1f9a7b3c5d2e8f1a6b",
  "lookup": "plate",
  "status": "resolved",
  "country": "DK",
  "mode": "live",
  "synthetic": false,
  "vehicle": {
    "avs": { "version": "0.3", "kind": "vehicle" },
    "identity": {
      "brand": { "name": "VW", "ids": [{ "scheme": "autouncle", "value": "b-0957579dc5bca6de229d2f32" }] },
      "model": { "name": "Golf", "ids": [{ "scheme": "autouncle", "value": "cm-6bb52590daf6d1b19108ec93" }] },
      "body": { "name": "Golf VIII", "ids": [{ "scheme": "autouncle", "value": "mg-40c5d8de40bb48ae60fc0a6c" }], "type": "hatchback", "fromYear": 2019 },
      "drivetrain": { "name": "1.5 TSI (130 Hp)", "ids": [{ "scheme": "autouncle", "value": "dv-947a3199394a5a4b41cb2869" }] }
    },
    "specification": {
      "body": { "type": "hatchback", "doors": 5 },
      "powertrain": { "energy": ["petrol"], "engine": { "displacementCc": 1500 }, "powerKw": 96, "powerHp": 130,
                      "transmission": { "type": "manual" }, "traction": "2wd" }
    },
    "vehicle": {
      "registration": { "plate": "AB12345", "country": "DK", "firstRegistrationDate": "2021-03-15", "modelYear": 2021 },
      "odometer": { "km": 38000, "readAt": "2025-03-02" },
      "colour": { "name": "Grå" }
    }
  },
  "identification": {
    "status": "identified",
    "level": "drivetrain",
    "catalogue_version": "5.6.9:NEW",
    "fields": {
      "/identity/drivetrain": { "basis": "catalogue", "source": "catalogue_match", "status": "derived", "match_score": 1.0 },
      "/specification/powertrain/powerHp": { "basis": "catalogue", "source": "catalogue_match", "status": "confirmed" },
      "/vehicle/odometer": { "basis": "observed", "source": "registry", "source_id": "dk_vehicle_register", "status": "supplied" },
      "/vehicle/registration/firstRegistrationDate": { "basis": "observed", "source": "registry", "source_id": "dk_vehicle_register", "status": "supplied" },
      "/vehicle/registration/plate": { "basis": "observed", "source": "provided", "status": "supplied" }
    }
  },
  "freshness": { "retrieved_at": "2026-11-01T15:04:05Z", "cached": false, "data_as_of": null, "dataset_version": null },
  "improve_precision": [],
  "warnings": [],
  "notes": [],
  "attributions": [
    { "source_id": "dk_vehicle_register", "name": "Danish Motor Register (DMR)", "url": null, "party": "third_party", "role": "vehicle_registry", "license": null, "attribution_text": null, "notice_status": "pending_review", "notice_version": "dk_vehicle_register@2", "...": "pointers to the values it supplied" },
    { "source_id": "autouncle_vehicle_catalogue", "...": "see notes and data sources" }
  ],
  "usage": { "billable": true, "credits_used": 1, "original_credits_used": 1, "credits_remaining": 29, "cache_hit": false, "idempotent_replay": false, "mode": "live" }
}
```

The example's `fields` and attributions are shortened; a real response lists every value.

## The lookup result

The three lookup endpoints return the same envelope, listed field by field under each endpoint above. Read two statuses:

- `status` is the source's answer: `resolved` (it returned vehicle data), `suggested` (only conditional answers; read the notes), `ambiguous` (the source holds competing answers), `no_match` (a valid input it does not know), or `unsupported` and `invalid_input` (it could not answer this input). Only the first three are billed. Test keys always get `resolved`.
- `identification.status` is how well the catalogue [identified](#status) what the source said.

`improve_precision` and `warnings` work as for valuations: a hint names the request field that would identify the vehicle more precisely, and a warning such as `registry_no_match` or `model_not_recognized` says what could not be used.

### Freshness

| Field | Type | Description |
| --- | --- | --- |
| `retrieved_at` | string (date-time) | When the source answered (UTC). A cached answer keeps its original time. |
| `cached` | boolean | `true` when the answer came from AutoUncle's lookup cache. |
| `data_as_of` | string or null | The source's own reference date, when it reports one. |
| `dataset_version` | string or null | Decoder and dataset release that produced the answer. |

## Attributions

Every source behind a published value is listed in `attributions`, with the publisher's own licence and attribution text, as the source publishes it. Show `attribution_text` wherever you show or store the data, and never shorten it. Sources that did not contribute a published value are not listed. See [notes and attributions](/docs/guides/notes-and-attributions).
