> ## Documentation Index
> Fetch the complete documentation index at: https://lmn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Versioned changes to the LMN Open API.

Subscribe via this page's RSS feed (Mintlify-generated) for release notifications. Public LMN Open API contract versions start at `v1.0`.

## v1.43 — 2026-06-26

**`GET /v1/vehicles/{id}` now recovers purchased dealer cars from your order snapshot after they sell or delist.**

When you request detail for a dealer (`encar_*`) vehicle that is no longer live (upstream 404, or advertisement status `SOLD` / `WAIT`), LMN now returns your stored **order-time vehicle snapshot** with `200 OK` instead of `404 vehicle_not_found` — provided you have already created an order for that car. A vehicle you've purchased therefore stays viewable (condition, inspection, photos, frozen pricing) after Encar removes the live listing.

* **Scoped to you.** The snapshot is matched on your partner account and the key's environment, so recovery applies only to cars *you* ordered. A delisted car you never ordered still returns `404 vehicle_not_found`.
* **Same payload as the order snapshot.** The returned `VehicleDetail` is the one captured at order creation (identical to `GET /v1/orders/{id}/vehicle`), with photo URLs mirrored to LMN-hosted storage.
* **No live call.** Snapshot responses don't hit Encar, so they're unaffected by upstream availability.
* **Live behavior unchanged when no snapshot exists.** Without a matching order, detail still resolves live and returns `404` / `503` exactly as before.
* **Live since 2026-06-15** (shipped alongside the v1.39 order-snapshot work, #212); this entry documents the contract. Additive — no fields renamed or removed.

## v1.42 — 2026-06-22

**Order resource and webhook `order.*` payloads now include a `pricing` breakdown.**

The `Order` resource returned by `POST /v1/orders`, `GET /v1/orders/{id}`, and `GET /v1/orders` (list) now carries a top-level `pricing` field with the same shape as `GET /v1/vehicles/{id}` `pricing`: `listing_price`, `sold_price`, `discount_config`, `breakdown` (including `estimated_landed`), `auction_fee_config`, `history`, and `market_assessment`. The snapshot is captured at order-creation time — it reflects the FX rate and cost config at the moment `POST /v1/orders` was called and does not change afterward.

* **Null for legacy orders.** Orders created before this release, and any order where the FX provider was unavailable at creation, return `pricing: null`.
* **Auction orders.** `history` is `[]` and `market_assessment` is `null` (auction sale history is not snapshotted onto orders). `auction_fee_config` is populated.
* **Dealer orders.** `auction_fee_config` is `null`; `breakdown.dealer_fee` reflects the dealer fee at creation time.
* **Webhook parity.** Per the v1.18 guarantee, `data.order.pricing` in every `order.*` webhook payload is byte-identical to `GET /v1/orders/{id}.pricing`. No separate webhook update is needed.
* **Backfill.** `POST /v1/orders/backfill` now returns a populated `pricing` for delisted dealer cars — the FX rate is locked at backfill time and landed cost is computed from the stored vehicle snapshot.
* **Additive.** No existing fields were renamed or removed. Clients that ignore unknown fields are unaffected.

## v1.41 — 2026-06-19

**`X-User-Id` is now persisted raw on orders (admin-visible) and logged raw alongside the hash.**

When `X-User-Id` is sent with `POST /v1/orders`, the raw value is now persisted as `external_user_id` on the order row and is visible on the LMN admin order detail page. It is **not** returned in the partner-facing `/v1` order response — the `Order` JSON shape is unchanged.

Additionally, every `/v1` request that carries `X-User-Id` now logs **both** the raw value (`external_user_id`) and the existing one-way SHA-256 hash (`external_user_id_hash`) in the audit log. Previously only the hash was captured.

This is a reversal of the prior hash-only stance (#121). No partner-visible response changes — the `Order` resource and all existing fields are unchanged. See [Authentication → X-User-Id](/authentication#x-user-id-optional-attribution) for the updated table.

## v1.40 — 2026-06-17

**Breaking (dealer source): `dealer_inspection.insurance_amount` is now whole USD, not KRW.**

`GET /v1/vehicles/encar_<id>` now returns `dealer_inspection.insurance_amount` as a whole-USD integer, FX-converted server-side — matching the auction `accident_cost_summary.insurance_paid` convention. Previously it was raw KRW.

* **Action required if you render this field:** stop applying `fx_rate` client-side and stop labelling it `₩` — the value is already USD. A car that previously reported `insurance_amount: 1850000` (₩) now reports `insurance_amount: 1370` (\$).
* **Unchanged:** `insurance_history` (own-damage claim count) is still an integer count; `null` still means "no data reported" and `0` still means "no claims".
* **Auction source unaffected** — its `accident_cost_summary` monetary fields were already whole USD.

## v1.39 — 2026-06-15

**Purchased vehicle snapshots for dealer orders.**

* Added `GET /v1/orders/{id}/vehicle`, returning the stored `VehicleDetail` snapshot captured when the order was created.
* Dealer (`encar_*`) order creation now stores a full vehicle-detail snapshot so partners can still view condition, inspection, and photos after Encar removes the live listing page.
* Dealer photos and image URLs inside inspection data are mirrored to LMN-owned `https://storage.googleapis.com/lmnauto-auction-data/...` objects before the snapshot is persisted.
* Added `404 order_vehicle_snapshot_not_found` for historical orders created before purchased-vehicle snapshots were available.

## v1.38 — 2026-06-12

**`X-User-Id` end-user attribution header documented.**

You can attach an optional `X-User-Id` header to any request to attribute it to the individual end-user/dealer in your org (per-dealer analytics/curation). No validation, no auth semantics — opaque metadata. LMN's request logs store a one-way SHA-256 hash of the value, never the raw string. See [Authentication → X-User-Id](/authentication#x-user-id-optional-attribution).

Capture has been live since 2026-06-09; this entry documents the contract. The header is not persisted on order resources.

## v1.37 — 2026-06-11

**Eagle Eye: four new filter fields, per-field flexibility, and match-type metadata on every response surface.**

### New filter fields (`filters` block, all Eagle Eye request bodies)

* **`fuel`** (`string`) — single lowercase fuel token. Accepted values: `gasoline`, `diesel`, `lpg`, `hybrid`, `gasoline_hybrid`, `diesel_hybrid`, `electric`, `plug_in_hybrid`, `hydrogen`. Multi-fuel is not supported — the dealer (encar) search DSL cannot express a fuel OR clause; supply exactly one token or omit. An array or unrecognized token returns `400 validation_error`.
* **`no_accident`** (`boolean`) — `true` filters to strictly clean cars. Semantics differ by source: dealer applies encar's strict 무사고 predicate (zero frame damage and zero exterior repair records); auction applies grade A (no structural exchange; exterior panel work is permitted). Not source-equivalent — see `no_frame_damage` for the portable alternative.
* **`no_frame_damage`** (`boolean`) — `true` excludes structural frame damage while permitting exterior repair. Source-equivalent: dealer maps to `Accident.N or Accident.F` (zero frame exchange); auction maps to grades A + B (no structural exchange). Recommended for frame-sensitive buyers across mixed sources.
* **`estimated_landed_max_usd`** (`number`) — maximum estimated landed cost in USD, computed per-partner using your FX rate and freight configuration. Vehicles whose landed cost cannot be computed are excluded. FX drift can move vehicles in and out of an active watch — set a margin or use `flexibility.fields.estimated_landed_max_usd`.

**No-inspection exclusion:** approximately 3% of dealer stock lacks a valid inspection record. When either damage filter is active, those cars are excluded (unknown condition is treated conservatively as not qualifying).

### New top-level `flexibility` block

An optional block that widens filter bounds and expands categorical alternatives without changing the dealer's stated criteria. When absent, all filters are applied strictly.

**Numeric / year fields** accept a `level` tolerance (`none` | `small` | `medium` | `large`):

| Level    | Max bound widening | Year slack |
| -------- | ------------------ | ---------- |
| `none`   | 0%                 | ±0         |
| `small`  | +2% (floor)        | ±0         |
| `medium` | +5% (floor)        | ±1 year    |
| `large`  | +10% (floor)       | ±2 years   |

**Categorical fields** (`model`, `trim`, `fuel`) accept `allowed_values` — each value adds one parallel search variant. `level: "none"` is an explicit no-op for per-field override of `flexibility.default`.

**Non-flexable fields** (`no_accident`, `no_frame_damage`, `source`, `make`) accept `level: "none"` only.

**Caps:** max 4 `allowed_values` per categorical field; max 8 total variant product across all categorical fields. Violations return `400 eagle_eye_invalid_flexibility`.

A `PUT /watches/{id}` without a `flexibility` key clears it to `null` (full-replace semantics).

### New match metadata on every response surface

`match_type` (`"exact"` | `"flex"`) and `flex_detail` are now present on:

* Search rows from `POST /v1/eagle-eye/search`
* Match list items from `GET /v1/eagle-eye/watches/{id}/matches`
* `additions` and `price_changes` entries in `eagle_eye.match` webhooks

`flex_detail` is a keyed object — one entry per field that required the tolerance:

* **Numeric entry:** `{ requested, level, effective, actual }` (all numbers)
* **Categorical entry:** `{ requested, allowed_values, actual }` (all strings)

`match_reason` (`filters` / `signals.*`) is unchanged — it remains orthogonal to flex classification.

### New error code

* **`eagle_eye_invalid_flexibility`** (`400`) — `flexibility` block is structurally invalid: field key references an absent filter, non-`none` level on a non-flexable field, `allowed_values` on a non-categorical field, categorical non-`none` level without `allowed_values`, or variant cap exceeded. `details.field` names the offending field.

See [Eagle Eye filter reference](/endpoints/eagle-eye#filter-reference) and [Eagle Eye overview — flexibility](/eagle-eye#buyer-fit-tolerance-flexibility) for full documentation and worked examples.

## v1.36 — 2026-06-11

**Dealer (`encar_*`) `WAIT` advertisements are now rejected as unavailable, like `SOLD`.**

v1.35 made detail and order creation reject dealer cars whose upstream advertisement status is `SOLD`. The same treatment now applies to status `WAIT`: `GET /v1/vehicles/encar_<id>` and `POST /v1/orders` return `404 vehicle_not_found`.

A 100-car sample (2026-06-11) confirmed every `WAIT` listing is dead inventory: Encar renders its own "sold or deleted" page and its live vehicle API returns 404 for them — only the stale embedded detail-page state still carries vehicle data, which previously let these cars appear buyable.

* **No response-shape change.** Active (`ADVERTISE`) cars are unaffected.
* **Operational meaning unchanged:** `404 vehicle_not_found` on an `encar_*` detail/order still means "not available to buy — don't retry."

## v1.35 — 2026-06-10

**Dealer (`encar_*`) detail and order creation now reject sold upstream advertisements as unavailable.**

### Partner-visible behavior change (dealer source only)

`GET /v1/vehicles/encar_<id>` and `POST /v1/orders` with a dealer `vehicle_id` now return `404 vehicle_not_found` when Encar's detail page is still reachable and parseable but the embedded advertisement status is `SOLD`. Previously detail could return a successful `VehicleDetail`, and order creation could create an order, for a car that was no longer active dealer inventory.

* **Affected endpoints:** `GET /v1/vehicles/{id}` for dealer IDs and `POST /v1/orders` when `vehicle_id` is `encar_<numeric>`.
* **Successful response shapes unchanged:** active dealer cars still return the same `VehicleDetail` / `Order` objects; no fields were added or removed.
* **List/search unchanged:** `GET /v1/vehicles` response shape and filters are unchanged.
* **Operational meaning:** treat `404 vehicle_not_found` on an `encar_*` detail/order as "not available to buy" — either the upstream returned 404 or the upstream detail page marked the advertisement as sold. Retry/backoff is still appropriate for `503 dealer_upstream_unavailable`, not for this 404.

## v1.34 — 2026-06-09

**`options_include` now filters dealer (`encar_*`) rows server-side.**

Previously, `options_include` was **silently dropped** for `source=dealer` — the filter was neither applied nor surfaced, so `source=dealer&options_include=sunroof` returned every matching dealer car, not just sunroof-equipped ones. It is now **applied** for dealer rows via Encar's native `Options.` search DSL, with the same **AND semantics** as auction (a car must have **all** listed options). A mixed `source=glovis,dealer` request now honors `options_include` on **both** halves.

**Two tokens are unsupported for dealer and now return `400 validation_error`** (`details.field: "options_include"`, with the offending token named): **`panoramic_sunroof`** and **`dashcam`**. Encar publishes no search filter for these, so rather than silently drop them, the request fails fast. This `400` is **dealer-scoped** — auction-only requests are unaffected, and a mixed request carrying one of these tokens returns `400` (not a degraded auction-only `200`). All other documented `options_include` tokens work for dealer.

No response-shape change. If you were relying on the previous silent no-op (sending `options_include` to dealer and ignoring it), your dealer result set will now be narrower; drop the param if you don't want option filtering.

## v1.33 — 2026-06-05

**Dealer (`encar_*`) `inspection_report.inspector_notes` is now populated (English).**

The Encar inspector's free-text opinion (특기사항 및 점검자의 의견) was previously dropped to `null` on dealer responses: it's raw Korean, and the partner-response Hangul scrub removes untranslatable Korean. It is now translated to English via Google Translate on the dealer fetch path — the same English-everywhere experience auction sources already provide — so the note (e.g. *"Partial putty/paint and corrosion; replaceable frame parts not disclosed. Over 200,000 km — self-warranty. Driver-side inside-panel damage."*) reaches the partner.

No shape change — `inspector_notes` is still `string | null`. It remains `null` only when the source genuinely has no opinion note or the inspection couldn't be fetched. Translation is best-effort: on a transient translate failure the note is omitted rather than shown in Korean.

## v1.35 — 2026-06-17

**New optional order status `inspection_ready`** — the inspected path is now `placed → inspection_in_progress → inspection_ready → acquiring`.

### Non-breaking additive (new enum value on `Order.status`)

* **`inspection_ready`** means the optional pre-acquisition inspection has completed and LMN can either continue to acquisition (`inspection_ready → acquiring`) or cancel for inspection failure (`inspection_ready → cancelled`, `cancellation_reason: "inspection_failed"`).
* **`inspection_in_progress` no longer exits directly to `acquiring` or `cancelled`** in the finalized inspection flow. Admins mark the inspection complete first, then take the acquisition/cancellation decision from `inspection_ready`.
* **LMN-owned** — partners cannot set `inspection_ready`. Attempting it via `POST /v1/orders/{id}/status` returns `403 invalid_status_transition` with `details.reason: "partner_not_authorized"`, alongside other LMN-owned statuses.
* `order.status_changed` webhooks now fire for `inspection_in_progress → inspection_ready` and for exits from `inspection_ready` (`inspection_ready → acquiring`, `inspection_ready → cancelled`).

This is **additive** — no fields added or removed, no partner-driven transition changed. Clients with a strict `status` enum **MUST tolerate** the new value (handle unknown enum values defensively, per [conventions](/conventions)).

## v1.32 — 2026-06-05

**`inspection_report.accident_cost_summary` now carries a per-incident `incidents[]` breakdown.**

### Added (additive — no fields removed, no shape change to existing fields)

* **`inspection_report.accident_cost_summary.incidents[]`** — one entry per insurance-settled accident (보험사고이력 : 내차 피해): `{ date: string | null, insurance_paid, repair_cost: number | null }`. All monetary values are whole USD, already FX-converted server-side. Currently populated for **Glovis**; other sources return `[]` (no per-incident detail upstream).

The aggregate **`insurance_paid` equals the sum of `incidents[].insurance_paid`** — rounding to whole USD is applied per incident and then summed, so a per-incident list always reconciles exactly to the total. This lets partners render "N accidents → payout per accident → total" without the figures disagreeing. Existing consumers that only read the aggregate fields are unaffected.

## v1.31 — 2026-06-03

**New optional order status `inspection_in_progress`** — the order lifecycle is now **10 statuses** (was 9), with `inspection_in_progress` positioned between `placed` and `acquiring`.

### Non-breaking additive (new enum value on `Order.status`)

* **`inspection_in_progress`** is an **optional** pre-acquisition state. Inspection is not mandatory: `placed → acquiring` directly remains valid for orders that skip inspection; `placed → inspection_in_progress → acquiring` is the inspected path. So `placed` now has two forward paths.
* **LMN-owned** — partners cannot set it. Attempting it via `POST /v1/orders/{id}/status` returns `403 invalid_status_transition` with `details.reason: "partner_not_authorized"`, alongside `acquiring`, `secured`, `export_processing`, `in_transit`.
* **Inspection failure resolves to `cancelled`** (reason `inspection_failed`), **never** `failed`. `inspection_in_progress → failed` is not a valid transition.
* `order.status_changed` webhooks now fire on entry to and exit from `inspection_in_progress` (`placed → inspection_in_progress`, `inspection_in_progress → acquiring`, `inspection_in_progress → cancelled`) and may carry `inspection_in_progress` in `status` / `previous_status`.

This is **additive** — no fields added or removed, no partner-driven transition changed. Clients with a strict `status` enum **MUST tolerate** the new value (handle unknown enum values defensively, per [conventions](/conventions)).

## v1.30 — 2026-06-03

**Dealer (`encar_*`) vehicles now populate `vin`, `transmission`, `engine_cc`, and `trim`.**

### Partner-visible response change (dealer source)

These four fields were previously always `null` for dealer (`encar_*`) listings. They are now resolved from the encar listing and its inspection report:

* **`transmission`** (`auto | manual | cvt | dct`) and **`engine_cc`** — from the listing spec.
* **`trim`** — now uses encar's canonical English grade name (a Korean grade that previously failed translation was being dropped to `null`).
* **`vin`** — from the listing when present, otherwise promoted from the inspection report's 차대번호 (Section 1).

`vin` is still typed `string | null`. It is `null` only when encar publishes no VIN for the listing — a minority of cars (e.g. listings whose inspection is an abbreviated record without the structured form). Across a random dealer sample, \~98% of cars now carry a VIN. **Treat `vin: null` as "not available from source"** and request the VIN from the dealer where your flow requires it (e.g. funding) — do not infer it.

No fields were added or removed and the contract shape is unchanged — these fields simply carry data for dealer rows now. Auction (`glovis`/`sk`/`aj`/`lotte`/`kcar`) behaviour is unchanged.

## v1.29 — 2026-05-28

**`pricing.breakdown.ocean_freight` now varies by vehicle — it is no longer a flat \$1,300 constant.**

### ⚠️ Partner-visible response change (both auction and dealer sources)

`ocean_freight` (in `pricing.breakdown`, on both `VehicleSummary` and `VehicleDetail`) is now resolved per vehicle from its body type and maker:

* **Mercedes-Benz** — `1800` for any body type.
* **Other makers, SUV-class** (SUV, minivan, van, truck) — `1800`.
* **Other makers, sedan-class** (sedan, compact, wagon, coupe, convertible) — `1500`.
* **Unknown / missing body type** (non-Benz) — `1500`. Dealer (`encar_*`) cars carry no body type, so they resolve via the Benz rule or this default.

The previous flat `1300` is no longer returned by any vehicle. `estimated_landed` reflects the per-vehicle freight accordingly. **Read `pricing.breakdown.ocean_freight` from each response rather than hardcoding a value** — the rates are operator-tunable and may change without a contract version bump. LMN commission ($300), dealer fee ($300), and discount mechanics are unchanged.

## v1.28 — 2026-05-28

**Inspection report reaches full dealer parity.** The dealer (`encar_*`) `inspection_report` now carries every section our own consumer site renders. No fields were removed — `category_grades[]` and `issues[]` are retained for backward compatibility.

### Added

* **`inspection_report.inspection_sheet_url`** (`string | null`, all sources) — link to the source's original inspection-sheet PDF. Populated for SK/AJ auction sources; `null` elsewhere (including dealer).
* **Dealer (`encar_*`) full parity.** `inspection_report.dealer_inspection` now additionally carries:
  * `vehicle_info` (object) — inspection-form-specific particulars only (`Valid Period`, `First Registered`, `Warranty`, `Engine Model`, `Base Price`), keys+values EN-translated. Identity fields (make/model, year, plate, VIN, transmission, fuel) are omitted — read those from the top-level resource.
  * `overall_status[]` (`{item, status, detail}`) — section-2 rows (emissions, tuning, special history, usage change, recall, odometer, VIN marking, color, major options), EN-translated.
  * `photos[]` (`{url, label}`) — inspection front/rear photos.
  * `certification` (`{inspector, notifier, date}` | null) — inspector/notifier raw Korean company names; date translated.
  * `registration_no` (`string | null`) — 성능번호 inspection registration number.
* **`inspection_report.inspector_notes` now populated for dealer** (previously `null`) — raw Korean free-text passthrough.
* **`dealer_inspection.insurance_history` / `insurance_amount` now populated** from Encar's insurance-record API (own-damage accident count + cost in KRW) — previously always `null`. Adds one upstream call to the dealer detail path; failures degrade to `null` and never block the response.

With this, a partner can render the **same** inspection report for dealer cars that lmnauto.com draws (vehicle info, overall status, accident/repair, exterior/frame diagram, mechanical checklist, inspector notes, photos, certification, insurance history).

## v1.27 — 2026-05-28

**Clean dealer (`encar_*`) cars now return a populated `inspection_report` (was `null`), and dealer `checklist[]` is now populated.** A dealer car with no panel damage previously returned `inspection_report: null` — partner UIs showed "No inspection report available" even though the inspection existed and was simply clean. Root cause: the server discarded any inspection that had no damaged panels and no frame/panel status row, conflating "no damage" with "no data."

### Partner-visible response change (dealer source only)

* **Clean cars now return a report.** `GET /v1/vehicles/encar_*` returns a populated `inspection_report` whenever Encar's inspection page is reachable. A clean car reads as zero damage (`dealer_inspection.critical_frame_damage = 0`, `exterior_damage = 0`, `has_accident = false`, `accident_summary = "No structural damage"`) with a populated mechanical `checklist[]`. `inspection_report` / `body_condition` are `null` only when Encar genuinely has no record (upstream 404) or a transient fetch failure occurs.
* **`inspection_report.checklist[]` is now populated for dealer cars** — up to \~35 entries `{ group, item, result }` covering the mechanical systems (engine, transmission, drivetrain, steering, braking, electrical, fuel), KO→EN translated with the same government-form dictionary used for auction sources and the consumer site. Every form row is passed through; an item the inspector left unmarked has an empty-string `result`.
* **New field `inspection_report.dealer_inspection.simple_repair`** (`boolean | null`) — Encar's 단순수리 (simple repair) flag. `null` when the page doesn't state it. Additive, non-breaking.
* **`has_accident` now also reflects the accident-history block**, not just frame-damage counts — a car with a cleanly replaced/repaired part (no panel-diagram marker) but recorded accident history now correctly reports `has_accident: true` / `accident_summary: "Accident history reported"`.
* **Auction sources too: a clean car now returns a non-null `body_condition`.** Previously an auction car whose inspection found no damage and had no pre-rendered diagram image returned `body_condition: null`; it now returns `{ image_url, panels: [] }`. `null` is reserved for cars with no inspection data at all. (Glovis was unaffected — it always carries an `image_url`.)

## v1.26 — 2026-05-27

**`fuel` multi-word values now snake\_case.** Multi-word fuel tokens previously leaked an upstream space-form (`"gasoline hybrid"`, `"plug-in hybrid"`) into the partner contract — partner-visible in `VehicleSummary.fuel`, in `/v1/vehicles/facets` `fuels[]`, and in any URL that needed encoding. Aligned to the `options_include` snake\_case convention.

### ⚠️ Partner-visible response change (both auction and dealer sources)

`VehicleSummary.fuel` now emits snake\_case for every multi-word value across **all sources** — auction (`glovis_*`, `aj_*`, `kcar_*`, etc.) AND dealer (`encar_*`). Previously the auction-side path bare-lowercased the BQ-stored string (`"gasoline hybrid"` survived), while the dealer-side path went through a Korean→English reverse map that emitted TitleCase-then-lowercase (also `"gasoline hybrid"`). v1.26 routes both through the same `toPartnerFuelToken` canonicalizer, so both emit `"gasoline_hybrid"` going forward.

* **Affected values:** `gasoline_hybrid` (was `"gasoline hybrid"`), `diesel_hybrid` (was `"diesel hybrid"`), `plug_in_hybrid` (was `"plug-in hybrid"`), plus future multi-word values surfaced by facets (`gasoline_plug_in_hybrid`, `lpg_plug_in_hybrid`, `dual_fuel_lpg` — emission only; filter for these still no-ops until follow-up PRs land their FUEL\_MAP entries).
* **Single-word values unchanged:** `gasoline`, `diesel`, `hybrid`, `electric`, `lpg`, `hydrogen`.
* **Why no deprecation period:** the space-form was never documented in `schemas.mdx`. Partners writing strict-enum validators against the published contract weren't accepting the space-form anyway; partners reading values from facets or accepting whatever the response carried receive the new form transparently.
* **Unknown-fuel fallthrough also now canonicalizes.** If encar emits a brand-new fuel token before we update FUEL\_MAP, the partner receives the snake\_cased form (e.g. `"new fuel type"` → `"new_fuel_type"`) instead of the bare lowercased raw — keeps the format invariant intact even for forward-compat surface.

### Other surfaces

* **Facets.** `/v1/vehicles/facets` `fuels[]` now emits the same snake\_case canonical form. Partners that read facets to build filter UIs receive values that work as `?fuel=` filter inputs without further transformation.
* **Filter input is lenient.** `?fuel=` accepts both the new snake\_case form AND the legacy space-form (URL-encoded `gasoline%20hybrid` still resolves correctly), as well as TitleCase. Mintlify documents only the snake\_case form going forward; the lenient input is internal robustness, not a documented alias.
* **`Order` resource inherits the fix automatically** — `Order.fuel` is computed at read time from the vehicle resource, so any hydrogen Nexo order placed during or after v1.26 reflects the new format on both the synchronous response and the webhook `data.order` payload (v1.18 invariant preserved).

## v1.25 — 2026-05-27

**Hydrogen added to the `fuel` enum.** `gasoline | diesel | hybrid | electric | lpg | hydrogen` — partners can now filter for hydrogen vehicles (Hyundai Nexo and similar FCEVs) via `?fuel=hydrogen`, and the response `fuel` field returns `"hydrogen"` instead of leaking the raw upstream Korean `"수소"`.

* **What broke before.** `?fuel=hydrogen` was silently dropped (no matching forward map entry), so partners filtering for hydrogen received unfiltered mixed-fuel results. Separately, dealer responses with a Korean source token would emit `fuel: "수소"` to the partner — partner UIs with strict enum validators rejected the row.
* **What's fixed.** Both directions covered by a single FUEL\_MAP entry: lowercase `hydrogen` translates to encar's `수소` upstream, and `수소` translates back to `hydrogen` in responses.
* **Demand.** Discovered via partner inventory probes on 2026-05-27 — facets endpoint was already advertising `hydrogen` as a valid value because two Hyundai Nexos sat in dealer inventory; partners reading facets would reasonably expect the filter to work.

## v1.24 — 2026-05-27

**Dealer (`encar_*`) inspection data now populated on `GET /v1/vehicles/encar_*`.** The existing `inspection_report` and `body_condition` fields are no longer mostly-empty on dealer cars — they now carry the same per-panel damage data that encar's own consumer site renders.

* **Partner contract unchanged.** Same shape, same field names. Partners reading `inspection_report.dealer_inspection.{critical_frame_damage,exterior_damage,frame_status,panel_status}` and `body_condition.panels[]` start seeing populated data on cars where it was previously null. No client changes required.
* **New data source.** Internally we switched from encar's JSON inspection endpoint (which was empty for most dealer listings) to encar's HTML inspection page (which encar's own UI uses). Same parser as our consumer apps — single source of truth, no risk of drift between partner API and `lmnauto.com`.
* **Coverage.** Cars with a posted inspection report now return populated data. Cars with no posted inspection (a small minority, typically dealer-direct listings) still return `inspection_report: null` and `body_condition: null` — same as today.
* **Insurance fields (`insurance_history`, `insurance_amount`) stay `null` for now.** The HTML page does not expose them in structured form; we'll surface them in a future release if we wire up encar's separate carHistory API.

Triggered by a partner reporting that dealer cars had no inspection data to render. Now they do.

## v1.21 — 2026-05-26

**`Order.options[]` added — vehicle feature names are now snapshotted onto the order.** The Order resource now includes an `options` array of English Title-case feature names (e.g. `Sunroof`, `Leather Seats`, `LED Headlamps`) — byte-equal to `VehicleDetail.options` on `GET /v1/vehicles/{id}` for the same vehicle. Partners no longer need a follow-up vehicle lookup to read feature information after creating an order.

* **Captured at order creation.** The list is snapshotted from the live vehicle at `POST /v1/orders` time (BigQuery for auction sources, encar detail parse for `encar_*` dealer sources) and stored on the order row, mirroring the existing `vin` / `license_plate` snapshot pattern.
* **Webhook parity.** Per the v1.18 guarantee, the same field appears on `data.order` in every `order.*` webhook event — no separate update needed.
* **Format.** English Title-case strings for both auction and dealer rows. Dealer (`encar_*`) values are translated through the same dictionary that powers the consumer site, so what you receive on the API matches what's rendered at `lmnauto.com/.../encar_*`. (The `options_include` *query parameter* on `GET /v1/vehicles` still uses snake\_case tokens — that's a separate filter input, not a response format.)
* **Backfill behavior.** Orders created before v1.21 return `options: []`. We do not retroactively populate; the snapshot is creation-time.
* **No breaking changes.** Purely additive. Consumers that ignore unknown fields continue to work; consumers that want to read `options` can do so immediately on new orders.

### v1.21 hotfix (same day, included in same release)

Between the initial v1.21 deploy and partner announcement we caught that dealer (`encar_*`) `VehicleDetail.options` and the resulting `Order.options` snapshot were emitting raw Korean strings (e.g. `선루프`) instead of English (`Sunroof`) — inconsistent with the consumer site and the auction-source path, which both translate. Fix:

* `GET /v1/vehicles/encar_*` and the order-creation snapshot now both apply the `ENCAR_OPTION_KO_EN` dictionary used by the consumer site. New dealer orders will carry English names.
* One-shot DB migration (`0065_backfill_dealer_order_options_en.sql`) translates the handful of dealer-order rows that snapshotted Korean strings during the brief window before the fix.

## v1.20 — 2026-05-26

**Landed-cost preview on list view.** `GET /v1/vehicles` now returns `pricing.discount_config` and `pricing.breakdown` on every row of `VehicleSummary`. Same shape, same values as the detail endpoint — your list-view cards can render estimated landed cost (and the underlying line items) without a follow-up `GET /v1/vehicles/{id}` per row.

* **Strictly additive.** No fields renamed or removed. Existing list-view consumers see new fields appear; ignoring them is safe.
* **Parity guaranteed.** For a given vehicle, `summary.pricing.discount_config` and `summary.pricing.breakdown` are byte-equal to the same fields under `vehicle.pricing` from the detail endpoint.
* **`auction_fee_config` remains detail-only.** The fee schedule is constant per source/category, so you can hold a static copy locally rather than receive it on every list row.
* **Pilot constants still apply.** LMN commission $300, ocean freight $1,300, dealer fee/discount \$300 each — same as v1.16. Future versions will externalize these per partner/market/source.

Triggered by ValU's 2026-05-26 ask to surface breakdown + discount on the list response.

## v1.19 — 2026-05-26

**Dealer (`encar_*`) listings are now orderable.** `POST /v1/orders` accepts `vehicle_id` values with the `encar_` prefix, completing the dealer source loop that started with v1.15 (read) and v1.16 (pricing).

* **Buy-now semantics.** Dealer listings are fixed-price — `max_bid_amount_usd` MUST be omitted or `null`. Non-null dealer values return `400 validation_error`.
* **No auction cutoff.** Dealer orders have `auction_date: null` and `order_cutoff_at: null`. The `past_order_cutoff` error does not apply.
* **No competitive bidding.** Responses always carry `is_highest_bid: null` and `current_max_bid_usd: null` (dealer orders are not bid against other partners).
* **New error code: `dealer_upstream_unavailable` (503)** can now surface on `POST /v1/orders` — fired when the encar detail upstream times out, returns 5xx, or returns an unexpected response shape during order creation. Retry-safe; the order is not created. Upstream 404 still maps to `404 vehicle_not_found` (listing removed).
* **Idempotency, duplicate-order, and FX-lock semantics are unchanged** — the same `Idempotency-Key` rules apply, `(api_key, vehicle_id)` active-order uniqueness is enforced, and `fx_rate` is locked at creation. Settlement-side semantics (no auction-fee config, dealer fee already in `pricing.breakdown.dealer_fee`) follow the v1.16 dealer pricing shape.

No breaking changes. Auction order behavior is unchanged.

## v1.18 — 2026-05-20

**Webhook payload ⇔ GET response parity.** The `data.order` field of every `order.*` webhook event is now structurally identical to the response of `GET /v1/orders/{id}`. Same fields, same types, same enrichment.

* **`is_highest_bid` and `current_max_bid_usd` now populated in webhook payloads** (previously always `null`). They are computed at emit time against the active bid landscape, matching the GET endpoint exactly.
* **Implementation guarantee:** both code paths route through the same response mapper, and an invariant test enforces structural equality on every PR. Partners can reuse a single deserializer across both channels.
* **No breaking changes:** if your handler was tolerating `null` for these two fields, it'll continue to work. Webhook consumers who *also* call `GET /v1/orders/{id}` can drop the redundant call.

## v1.17 — 2026-05-13

**Order state-machine refactor: 9-state linear lifecycle.** Replaces the 6-state model + `fulfillment_detail` field with explicit per-phase statuses. Partner integration must update.

* **`proceed` renamed to `placed`** everywhere — order resource, filter enum, error messages, sandbox lifecycle.
* **`shipping` status removed.** Logistics phase is now expressed as three explicit statuses:
  * `export_processing` — Korean export workflow underway (LMN-set on entry from `secured`).
  * `in_transit` — Bill of Lading issued and vessel has departed Korean port (**LMN-set**, not partner-set as previously planned).
  * `customs` — vessel arrived at destination port; vehicle in customs-bonded area (partner-set via `POST /v1/orders/{id}/status`).
* **`fulfillment_detail` field removed** from the Order resource and from `POST /v1/orders/{id}/status` request bodies. Sending it returns `400 validation_error`. The `awaiting_pickup` enum value is dropped — folded into `export_processing`.
* **`order.fulfillment_updated` webhook event removed.** All logistics-phase changes now fire `order.status_changed` instead. Update any switch-on-event-type handlers.
* **Partner-driven status pushes simplified** to two transitions:
  * `in_transit → customs` (vehicle arrived destination port).
  * `customs → delivered` (terminal, **mandatory**).
* **State machine invariants tightened:**
  * `failed` is reachable from `acquiring` only. Post-`secured` failures are out of scope; they go offline.
  * `cancelled` is reachable from `placed` only. Post-`acquiring` cancellation goes offline.
* **Reason routing for `auction_cancelled` and `seller_withdrew`:** terminal is decided by the order's current status at the moment of the upstream event. Pre-`acquiring` discovery routes to `cancelled` (via `cancellation_reason`); post-`acquiring` routes to `failed` (via `failure_reason`). Same reason string, different field — webhook consumers should treat the *terminal* as authoritative.
* **Competing-order cohort rule:** when multiple partners hold `placed` orders on the same auction lot, all transition to `acquiring` together when LMN starts bidding. No order goes `placed → failed` directly.
* **Zombie cleanup:** orders stuck in `placed` past `auction_date + 24h` are auto-resolved to `failed` with `failure_reason: auction_passed`. The webhook collapses the synthetic intermediate state — `previous_status: "placed"` with an additive `synthetic_acquiring: true` discriminator. Treat as a normal `failed` event, or branch on the discriminator to suppress noise.
* **Reschedule auto-cancel scope:** `placed → cancelled` (`auction_rescheduled_too_soon`) only fires from `placed`. Orders already in `acquiring` proceed normally on the new auction date (a separate `order.auction_rescheduled` event still fires).
* **Error taxonomy clarified:** `invalid_status_transition` now distinguishes via HTTP status — `403` = partner attempted an LMN-owned status (`details.reason: "partner_not_authorized"`), `409` = sequence violation (`details.reason: "out_of_sequence"`). `details.current_status` set in both cases.

**Migration checklist for integrators:**

1. Replace literal status comparisons: `"proceed"` → `"placed"`; `"shipping"` → `"export_processing"` / `"in_transit"` / `"customs"`.
2. Remove all reads of `order.fulfillment_detail` (always `undefined` now).
3. Update `POST /v1/orders/{id}/status` request bodies to send `{"status": "customs"}` or `{"status": "delivered"}` — never `fulfillment_detail`.
4. Remove `case "order.fulfillment_updated"` from webhook handlers; logic moves into the `order.status_changed` handler.
5. Add support for the new `synthetic_acquiring: true` event-payload variant (or ignore it — partners can treat it as a normal `failed`).

## v1.16 — 2026-05-13

**Pricing shape rework: symmetric fees + fixed discount.** Tightens dealer pricing semantics and makes the fee model uniform across sources.

* **`pricing.breakdown.auction_fee` and `dealer_fee` are now always integers.** Branch on `source`, not on `null`.
  * Auction rows: `auction_fee` populated as before; `dealer_fee: 0` (was `null`).
  * Dealer rows: `auction_fee: 0` (was `null`); `dealer_fee: 300` flat USD (was the KRW-derived 440,000 KRW conversion).
* **`pricing.discount_config` is now always populated.** Was `null` for dealer.
  * Auction: bid-based fields unchanged.
  * Dealer: `bid_threshold`, `rate_below`, `rate_at_or_above` are all `0` (bid-based logic does not apply).
* **New field: `pricing.discount_config.fixed_discount_amount`** (whole USD). Flat discount subtracted from `estimated_landed` at quote time, independent of bid.
  * Auction: `0` today.
  * Dealer: `300` — exactly offsets the dealer fee. Net economic effect on `estimated_landed` is zero, but the two levers are now independent and can be adjusted separately.
* **`estimated_landed` formula updated** to subtract `fixed_discount_amount` in both auction and dealer paths. Auction landed values are unchanged today (fixed\_discount = 0); dealer landed is unchanged today (`+300 − 300` cancels).
* **Migration note for the recompute formula:** `your_total_discount = your_bid_rate_discount + fixed_discount_amount` (the bid-rate component is unchanged from v1.15).

## v1.15 — 2026-05-13

**New feature.** `source=dealer` is now functional — backed by a live proxy to a third-party retail inventory (encar). Roughly 10–18× expansion in accessible inventory for common Korean used-car queries compared to auction-only.

* **`source=dealer` is queryable.** `GET /v1/vehicles?source=dealer&make=<make>&...` returns dealer listings with `id` prefix `encar_<numeric>`, `source: "dealer"`, and a dealer-shaped `pricing.breakdown` carrying the dealer fee. (Pricing shape further refined in v1.16 — see above.)
* **Dealer constraints (new 400s):**
  * `source=dealer` (alone or mixed) requires `make`. Multi-value `make` rejected.
  * Combining `source=dealer` with any auction-only filter (`auction_status`, `accident_grade`, `exterior_grade`, `auction_date_*`, `auction_count_*`) returns `400 validation_error`.
  * Mixed `source=glovis,dealer` is first-page only — passing `cursor` returns `400 mixed_source_pagination_unsupported`. Paginate each source separately.
* **Filter parity gaps for dealer:** `transmission`, multi-value `fuel`, `keyword`, `body_style` are silently dropped from the dealer half (upstream DSL doesn't support them). Auction half of a mixed request still honors them. (`options_include` was also dropped at launch but now filters dealer rows server-side — see v1.34.)
* **New response headers:**
  * `X-LMN-Dealer-Page-Max: 48` — when `limit > 48` and dealer is in scope (upstream caps page size). Paginate dealer separately via `cursor`.
  * `X-LMN-Partial-Dealer-Unavailable: <csv>` — when at least one dealer source failed. The value is a comma-separated list of the failed dealer source names (`encar`, `danawa`, or `encar,danawa`). On a mixed request the 200 is still valid and auction is authoritative; on a pure dealer request a single source down still returns the survivor's rows + this header (only an all-sources-down dealer request returns 503).
* **New error codes:**
  * `400 mixed_source_pagination_unsupported` — see above.
  * `503 dealer_upstream_unavailable` — pure dealer request failed because the upstream is unreachable or returned an unexpected response shape. Mixed requests degrade instead of returning 503.
* **Detail endpoint accepts dealer IDs.** `GET /v1/vehicles/encar_<numeric>` returns dealer detail with auction-only fields (`comparables`, `body_condition`, `inspection_report`, `pricing.history`, `pricing.market_assessment`, `auction_fee_config`) all `null` or empty. `404 vehicle_not_found` only on upstream HTTP 404; other failure modes → 503. (`discount_config` shape further refined in v1.16 — now populated for dealer with zeroed bid-based fields.)
* **Mixed-source sort semantics:** When `sort=auction_date_asc` (the default) is used on a mixed query, the `limit` budget is split between auction (sorted by date) and dealer (sorted by `year` desc, since dealer rows have no auction date). Other sort options sort across the merged stream as before.
* **`/v1/vehicles/facets` `sources` array now includes `"dealer"`** when the upstream is reachable. Health is probed lazily and cached for 60 seconds; partners can treat the facets response as the canonical "currently queryable sources" signal.

## v1.14 — 2026-05-13

**Breaking change in `pricing.breakdown`.** `korean_export` removed.

* **`korean_export` removed** from `pricing.breakdown`. Korean-side export logistics are folded into `ocean_freight`. The field was previously deprecated (held at `0` for auction, `null` for dealer) but no production partner ever consumed the new shape, so the removal lands pre-launch with no migration window.
* Clients that read `pricing.breakdown.korean_export` will get `undefined`; remove those reads and rely on `ocean_freight` for the combined Korean-export + ocean-freight figure.

## v1.13 — 2026-05-12

**Breaking change in `pricing`.** Regenerate clients before consuming `GET /v1/vehicles/{id}`.

* **`korean_export` deprecated** in `pricing.breakdown` (superseded by v1.14 removal — see above). Held at `0` for auction vehicles, `null` for dealer vehicles. Korean-side export logistics are now folded into `ocean_freight`.
* **`lmn_commission` value changed:** `$1,500` → `$300`.
* **`ocean_freight` value changed:** `$2,500` → `$1,300` (now covers Korean export + ocean freight combined).
* **`pricing.discount_config` added** (sibling to `auction_fee_config`). Shape: `{ bid_threshold, rate_below, rate_at_or_above }`. The discount is **bid-dependent** — `bid_threshold` is compared against the partner's bid amount in USD, not the listing price. Partners apply `rate_below` / `rate_at_or_above` to their own bid to compute the discount for that bid; the **invoice realizes the same rule against the actual hammer price** (`purchase_price_usd`), which may be lower than `max_bid_amount_usd`. Current values: `{ bid_threshold: 25000, rate_below: 1.5, rate_at_or_above: 2 }`.
* **`pricing.breakdown.estimated_landed` semantics changed** to an *illustrative single-bid total*. Both bid-dependent fees (`auction_fee` and the discount derived from `discount_config`) are now evaluated against the same internal basis price LMN selects: `sold_price` if available, otherwise LMN's market-derived final-price estimate, otherwise the auction starting price. For exact totals at YOUR bid, recompute using `auction_fee_config` and `discount_config`. `estimated_landed` is no longer a quote; it is an estimate.
* See the Vehicles endpoint guide section **"Recomputing landed cost"** for the exact bid-level formula, rounding rules, and settlement caveats.

## v1.12 — 2026-05-11

**Classification: non-breaking infrastructure change.** No request/response shape changes. `fx_rate` contract is unchanged: still `number | null`, still locked at `POST /v1/orders`, still immutable across `PATCH` / status updates, still `null` only when the upstream FX provider was unavailable at creation time.

* **FX source unified.** `fx_rate` and all derived currency conversions now resolve through a single source — openexchangerates.org `/historical/{date}.json`, where `date` is yesterday in KST (D-1 close lock, KST midnight rollover). Previous behavior pulled USD/EUR/JPY/etc. from Frankfurter (ECB) and overlaid OXR for select managed-float currencies; consolidation removes that split.
* **`fx_rate: null` semantics unchanged.** When openexchangerates.org is unreachable at `POST /v1/orders`, the order still creates with `fx_rate: null` and LMN applies the settlement-day rate offline at `secured`, exactly as before.
* **No action required for partners.** No fields added or removed. No code regeneration needed.

## v1.11 — 2026-05-06

**`vin` added as a `photo_tags[].tag` value.** When the API can identify the photo that shows the VIN plate (차대번호), that photo is tagged `'vin'` in `photo_tags[]`. At most one `vin` tag per car. The position of the `vin`-tagged photo in `photo_tags[]` is **not** positional; it can appear at any index. Other tags remain positional heuristics from `photos[]` array order. The `'vin'` tag may be absent — partners should not assume every car has it. Backwards-compatible: clients that ignored unknown tag values continue to work; clients that enum-validated tags must regenerate from the OpenAPI spec.

## v1.10 — 2026-05-01

**Vehicle response contract update for the initial partner pilot.** Regenerate clients before consuming `GET /v1/vehicles` or `GET /v1/vehicles/{id}`.

* **Vehicle pricing stays in `pricing`, with USD as the documented unit.** Every monetary number in partner vehicle responses is a whole USD integer unless explicitly documented otherwise.
* **Nested vehicle pricing fields no longer use `_usd` suffixes.** Renames include:
  * `pricing.listing_usd` → `pricing.listing_price`
  * top-level `sold_price_usd` → `pricing.sold_price`
  * `pricing.breakdown_usd` → `pricing.breakdown`
  * `pricing.history[].price_usd` → `pricing.history[].price`
  * `pricing.similar_sales[]` → `comparables.past_sales[]`
  * `pricing.similar_upcoming[]` → `comparables.upcoming_sales[]`
  * `pricing.market_assessment.market_median_usd` → `pricing.market_assessment.market_median`
* **`pricing.delta_pct` was removed.** Use `pricing.history`, `comparables.past_sales`, and `pricing.market_assessment` for price movement and market context.
* **`pricing.auction_fee_config` is reduced to partner-useful fields only:** `{ percentage, minimum, maximum }`. `minimum` and `maximum` are source/category auction-fee caps converted to whole USD using the response FX snapshot.
* **`pricing.breakdown.estimated_landed` uses `sold_price` when available, otherwise `listing_price`.** This makes sold-vehicle landed cost reconcile inside the same `pricing` object.
* **`pricing.history[]` now returns same-car auction appearances.** Entries are matched internally by Korean license plate and include `vehicle_id`, `auction_date`, `price`, and `result`. `vehicle_id` is an auction listing/round ID, so it may differ between history rows for the same physical car. This is current-vehicle history, not comparable-car history.
* **Comparable vehicles moved to `comparables`.** `comparables.past_sales[]` is sold-only and `comparables.upcoming_sales[]` contains future auction alternatives. Both include `vin` and `license_plate` when available.
* **Vehicle detail now populates `body_condition`, `inspection_report`, and `photo_tags`.** `body_condition.panels[]` uses English labels only, and `body_condition.image_url` is returned only for LMN-mirrored media. `photo_tags[]` items are `{ url, tag }`; array order follows `photos`.
* **Sandbox lifecycle testing uses `POST /v1/orders/{id}/sandbox-status`.** The normal `POST /v1/orders/{id}/status` endpoint remains restricted to partner-owned fulfillment updates and `delivered` in every environment.
* **`vehicle.transmission` is now normalized to `auto | manual | cvt | dct`.** Previously the response surfaced raw upstream tokens (`A/T`, `M/T`, `CVT`, `DCT`); responses now match the `schemas.mdx` enum. The filter param `?transmission=` continues to accept both lowercase and raw forms — request shape is unchanged.
* **`vehicle.pricing.sold_price` is now strictly null unless `auction_result` is `sold` or `negotiation_sold`.** Stale upstream `soldPrice` values on `negotiation_requested` / `no_bid` / `upcoming` rows are no longer surfaced.
* **`vehicle.drivetrain` is now an open string in the spec.** Unknown drivetrain tokens pass through unchanged; codegen clients should treat the field as opaque rather than enum-rejecting.
* **`GET /v1/vehicles` filter docs:** `mileage_max_km` (upper bound) documented. *(Correction: an earlier draft of this entry listed `mileage_min_km`; it is **not** supported server-side — only `mileage_max_km` exists.)*
* **Vehicle response examples corrected.** `thumbnail_url` / `photos[]` examples now use the actual upstream `lmnauto-auction-data` GCS host that the API returns. The `inspection_report` example body now reflects the real mapper output (source / overall\_grade / grade\_description / accident\_summary / has\_accident / has\_frame\_damage / category\_grades / checklist / accident\_history / accident\_cost\_summary / issues / inspector\_notes) instead of the previous placeholder fields.

## v1.9.2 — 2026-04-30

**Non-breaking** (new filter + sandbox-only helper).

* **`GET /v1/vehicles` now supports `auction_status`.** Use `auction_status=upcoming` to fetch upcoming listings directly. Allowed values: `upcoming`, `sold`, `negotiation_sold`, `no_bid`, `negotiation_requested`.
* **Sandbox-only `POST /v1/orders/{id}/sandbox-status`.** Lets pilot partners simulate order status and fulfillment transitions to test webhook delivery. Available only on sandbox hosts with sandbox API keys.

## v1.9.1 — 2026-04-29

**Classification: non-breaking clarification.**

**No request/response model shape changes.** No endpoint path, method, required request field, response object field, or field type changed in this version.

**Added enum value**

* `Order.failure_reason` may now include `seller_withdrew`. This is an additive value on the existing nullable `failure_reason` field. Treat it as a terminal `failed` reason.

**Clarified enum documentation**

* `Order.fulfillment_detail` readable values are now documented as: `export_processing`, `awaiting_pickup`, `in_transit`, `customs`, `in_customs`, `at_destination_port`, `delivered`.
* Partner-writable `fulfillment_detail` values remain only `in_transit` and `customs` through `POST /v1/orders/{id}/status`.
* `status: delivered` remains the only partner-writable `status` value. Partners still cannot push `status: shipping`.

**Corrected examples only**

* Postman status-push examples no longer send invalid `status: shipping`; they now use `fulfillment_detail: in_transit`.
* Postman order-creation examples no longer include stale body field `idempotency_key`; idempotency remains the `Idempotency-Key` HTTP header.

**Corrected operational docs only**

* Webhook delivery testing now documents cron-window delivery instead of a hard 5-second expectation.
* Pilot webhook destination setup now documents the current `partner_webhook_configs` DB row with `secret_ref`; there is no partner-facing webhook config route yet.

## v1.9 — 2026-04-29

**Behavior change** (per-partner). Cutoff timing rule changes; partners using fixed cron schedules need to verify against the new value in `order_cutoff_at`.

* **`order_cutoff_at` is now `auction_date − N minutes` per partner.** Replaces the prior "auction\_date − 1 day, midnight KST" rule. N is configured per integration on the partner record. Default 1440 (24h) for inspection-heavy integrations; integrations without a secondary inspection step use a shorter lead time (e.g. 60 minutes) covering bid submission only. Stop computing cutoff client-side; always read `order_cutoff_at` from the response.

## v1.8.1 — 2026-04-27 (hotfix)

* **`thumbnail_url` now always returns a working URL.** Previously the field surfaced a derived `thumbs/images/...` path that 404'd because the upstream thumbnail-generation pipeline isn't built yet (integration-spec §10 #13). Partner API now returns the raw `images/...` URL directly — full-size, but always loads. Run your own resize CDN if list-view bandwidth matters.

## v1.8 — 2026-04-27

**Non-breaking** (additive event types + Order resource fields). Existing `order.status_changed`, `order.fulfillment_updated`, and `order.auction_rescheduled` handlers are unaffected; existing Order parsers ignore new fields.

* **`vin` and `license_plate` on Order resource.** Snapshotted at order creation (immutable for the life of the order). Webhook payloads now carry both fields without a follow-up `GET /v1/vehicles/{vehicle_id}` round-trip — match dealer records directly from the event. `vin` is null when the upstream listing has no VIN; `license_plate` is the Korean plate as-shown (e.g., `12가 3456`). Added per a partner integration commitment.
* **New webhook event: `order.price_updated`.** Fires when the vehicle backing an active `proceed` order has its `listing_usd` refreshed by the upstream scraper. Payload includes `previous_listing_usd`, `current_listing_usd`, `delta_usd`, `delta_pct`. Bid-strategy partners can use this to decide whether to PATCH `max_bid_amount_usd` mid-flight. Does not fire on terminal-state orders. See [`order.price_updated`](/webhooks/event-types#order-price_updated).
* **New webhook event: `order.re_auctioned`.** Fires when a vehicle from one of your terminal orders (`failed` / `cancelled` / `delivered`) reappears in a new auction round. Matched by Korean license plate so the same physical car is caught even when it moves between auction houses. Useful for offering the dealer a second chance. Deduped via `(partner_id, license_plate, new_auction_date)` so re-scrapes don't double-fire. See [`order.re_auctioned`](/webhooks/event-types#order-re_auctioned).
* **`GET /v1/orders/{id}/events` documented in `/endpoints/orders`.** The endpoint already shipped in earlier pilot builds; the partner-facing docs were missing. Returns LMN-side webhook delivery log (`delivery_status`, `attempts`, `last_response_code`, `payload`) for self-service "did I miss a webhook?" debugging. See [GET /v1/orders/{id}/events](/endpoints/orders#get-v1-orders-id-events--event-history).
* **OpenAPI `OrderEvent` schema corrected.** Previously declared fields (`event_type`, `from_status`, `to_status`, `actor`, `metadata`) that did not match the live response. Now reflects the actual shape (`type`, `delivery_status`, `attempts`, `last_response_code`, `next_retry_at`, `created_at`, `delivered_at`, `payload`). Codegen clients should regenerate.

## v1.7 — 2026-04-24

**Non-breaking** (additive fields + new endpoint). Pilot partners on v1.6 can keep working against the old response shape; the new fields are ignorable.

* **New endpoint: `PATCH /v1/orders/{id}`.** Update `max_bid_amount_usd` on an existing order. Allowed only while `status=proceed` and before `order_cutoff_at`. No `Idempotency-Key` required. See [PATCH /v1/orders/{id} — Update max bid](/endpoints/orders#patch-v1-orders-id--update-max-bid).
* **`fx_rate` is now locked at `POST /v1/orders` time** (previously: at `secured`). The KRW-per-USD rate is captured once at order creation, persisted on the row, and does not change via PATCH or status pushes. Gives partners a deterministic maximum settlement exposure from commit time. `null` on rare FX-provider outages.
* **Comparable sales on `GET /v1/vehicles/{id}`.** Up to 20 comparable cars (same make+model · year ±1 · mileage ±20,000 km · past 4 weeks). Excludes the target vehicle and any same-plate re-listings. See [Similar-sales definition](/endpoints/vehicles#similar-sales-definition).
* **`pricing.market_assessment` on `GET /v1/vehicles/{id}`.** One-line server-side price verdict with five labels (`great` / `good` / `fair` / `wait` / `limited`) over the sold comparable set, plus `price_drop` when the same vehicle has appeared in prior auction rounds. See [Market assessment](/endpoints/vehicles#market-assessment-pricingmarket_assessment).
* **`thumbnail_url` now populated** on both `GET /v1/vehicles` and `GET /v1/vehicles/{id}` (was always `null` pre-2.7). Derived from the first auction-bucket image. Returns `null` only when the listing has no images at all. Past-auction vehicles may surface a URL that 404s until a backfill runs — treat 404 as "no thumbnail", not a broken API.
* **`VehicleSummary.fuel` is now nullable.** Previously declared non-null in the spec but could be absent in rare upstream data. OpenAPI now marks it `nullable: true`; partners with strict enum validators must accept `null`. Post fuel-null crash fix deployed 2026-04-23.

## v1.6 — 2026-04-18

**Breaking for integrators still on v1.5 drafts.**

* **Dealer-deposit removed.** `amounts.dealer_deposit_usd`, `amounts.dealer_refund`, `amounts.cancellation_refund` no longer exist. `POST /v1/orders` is a bid commitment, not a deposit attestation; payment is owed only on `secured`.
* **Error codes split into resource-specific.** `not_found` → `vehicle_not_found` / `order_not_found`. `unauthorized` → `missing_api_key` / `invalid_api_key`. `forbidden` and `invalid_transition` unified to `invalid_status_transition` (with HTTP 403 vs 409 distinguishing partner-attempt vs state-machine).
* **New `422 idempotency_key_reused`.** Reusing an Idempotency-Key with a different `vehicle_id` now returns this distinct error rather than silently returning the original order.
* **`GET /v1/orders` filters extended.** Added `ids` (CSV batch lookup, max 100), `from`/`to` (half-open \[from, to) on `created_at`, replaces the previous `created_after`/`created_before`), `sort` enum (`created_desc` default, `created_asc`, `auction_date_asc`, `updated_desc`). Cursor is now sort-specific.
* **Facets response uses plural field names.** `fuels`, `transmissions`, `sources` (was `fuel`, `transmission`, `source`). Explicit "no per-value counts" — UIs surface plain values.
* **Timestamp convention made explicit.** Source UTC offset must always be preserved — never `Z`-normalized. Node `Date.toISOString()` flagged as not acceptable.

## v1.5 — 2026-04-16

QA + business review fixes: all USD amounts are whole dollars (not cents); idempotency keys are permanent (no TTL); API key length standardized to 32 chars; `fulfillment_detail` write permissions clarified (LMN: `export_processing`/`awaiting_pickup`; partner: `in_transit`/`customs` optional); added `auction_passed` `failure_reason` for zombie order cleanup; added `vehicle_unavailable` (410) for buy-now vehicles; clarified `cancellation_reason` vs `failure_reason` mutual exclusivity; added FX risk note (auction-time rate); added post-secured disputes note (offline only).

## v1.4 — 2026-04-16

Revised competitive bidding: all orders accepted (removed `bid_too_low`). Response includes `is_highest_bid` + `current_max_bid_usd`. `outbid_internally` happens at auction time, not at order creation.

## v1.3 — 2026-04-16

Add competitive bidding: `bid_too_low` (409) with `current_max_bid_usd` disclosure. New `failure_reason: outbid_internally`. Webhook fires when lower bid is replaced.

## v1.2 — 2026-04-16

Add `auction_fee_usd`, `failure_reason`, shipment fields to Order. Remove `fx_snapshot` from API. Updated webhook payload examples with realistic data per transition.

## v1.1 — 2026-04-16

Add `max_bid_amount_usd` to order creation. Add `auction_count_min/max` vehicle filters. Add `price_drop_pct`, `first_price_usd` to vehicle response. Full webhook spec.

## v1.0 — 2026-04-16

**6-state order model.** Collapsed 14-state lifecycle to 6 states: `proceed → acquiring → secured/failed → shipping → delivered` + `cancelled`. Removed inspection from order lifecycle (secondary inspection is optional post-purchase service). Removed `POST /v1/orders/{id}/decision` and `GET /v1/orders/{id}/inspection` endpoints. Added `fulfillment_detail` field for granular shipping tracking. Added `order_cutoff_at`. Renamed `eta_lagos` → `eta_destination` with `destination_port`. Removed `inspection_fee` from Order amounts. Simplified cancellation to `proceed`-only via DELETE.
