Skip to main content
Subscribe via this page’s RSS feed (Mintlify-generated) for release notifications. Public LMN Open API contract versions start at v1.0.

v2.0 — 2026-07-11 (shipped)

⚠️ Breaking: every inspection_* token renamed to secondary_inspection_*. One coordinated rename, no dual-emit window — old names stopped working when this version deployed (sandbox first, production on 2026-07-11). “Secondary inspection” is the pre-purchase inspection LMN performs on request, distinct from the auction house’s own inspection sheet (inspection_report, which is unchanged).
This entry is pinned above later additive releases for visibility: v1.49–v1.51 below shipped after this cut and already use the new vocabulary.
Full mapping: Migration notes:
  • The legacy request key is rejected, not ignored. POST /v1/orders with inspection_required in the body returns 400 validation_error with a pointer to this entry — it is never silently dropped, so a stale client cannot accidentally create a no-inspection order.
  • Stored data migrates with the cut. Orders already in inspection_in_progress / inspection_ready re-read as the new status values; cancellation_reason: inspection_declined re-reads as secondary_inspection_declined. IDs, timestamps, and semantics are unchanged.
  • Webhooks: events enqueued before the cut and not yet delivered are rewritten to the new tokens before delivery — your handler only ever sees one vocabulary per environment. Match on the new status strings and payload keys.
  • Unchanged: order_cutoff_at, inspection_report / dealer_inspection / body_condition on the Vehicle resource, all endpoint paths (still /v1/…), auth, idempotency semantics, and every other field.
Why: the platform vocabulary for this workflow is “secondary inspection” end-to-end; the API previously used the shorter name, and the mismatch caused real integration and support confusion. One breaking rename now, while the integration surface is small, beats living with two vocabularies permanently.

Docs — 2026-08-07 (no API change)

Documented: self-serve sandbox onboarding, the trial call budget, and the X-Quota-* headers. Sandbox access stopped being email-gated on 2026-07-28; these docs still described the old flow until today. Nothing in the API changed on this date — the behavior below has been live since that deploy.
  • Getting a key is self-serve. Sign up at console.lmnauto.com with a work email and company name, auto-approved, no egress IP required. See Quickstart → Get a sandbox key. The old “email us your egress IPs” onboarding is retired.
  • Sandbox trial keys have no IP allowlist, so serverless and dynamic-IP backends work without pre-registration. Production keys remain allowlisted. See Authentication.
  • Trial keys carry a lifetime call budget (100 by default, 2 active keys per trial account). Budgeted keys receive X-Quota-Limit and X-Quota-Remaining on every response; unbudgeted keys receive neither.
  • 429 rate_limited is overloaded, and the two cases need opposite handling. It signals either the Eagle Eye test-webhook throttle (5 fires/min per watch — transient, clears in 60s) or an exhausted trial call budget (permanent, never clears). Neither sends Retry-After. Discriminate on error.details: present (limit + used) means budget, absent means throttle. Prior docs described a general burst/sliding-window limiter and prescribed Retry-After + exponential backoff — no such general limiter is enforced on /v1, and blanket backoff loops forever on the budget case. Now documented in Telling the two 429s apart, Test webhook throttle, Trial budget exhausted, and Authentication.
  • Corrected: key rotation has no overlap window. Authentication previously said old and new keys are both accepted “during a brief overlap window for zero-downtime cutover.” They are not — Keys → Rotate revokes the old key before returning the new one, so the old key is dead before you can deploy the replacement. For a genuinely zero-downtime rotation, create a second key, deploy it, then revoke the first: Rotating without downtime.
  • New guide: Build with AI — connect the docs MCP server (https://lmn.mintlify.app/mcp) to Lovable, Cursor, or Claude and build a server-side integration.
Existing partners are unaffected: keys issued before the self-serve launch have no call budget, send no quota headers, and keep their allowlists.

v1.56 — TBD (live on sandbox with the next deploy)

Trial call budget raised: new sandbox keys now carry 1,000 calls (was 100). The lifetime call budget on self-serve trial keys is 10× larger — enough to build, debug, and demo an integration without rationing requests. Everything else about the budget is unchanged: it is lifetime (not per-month), advertised via X-Quota-Limit / X-Quota-Remaining on every response, and exhaustion returns 429 rate_limited.
  • Applies to keys minted after this release. An existing trial key keeps the budget it was minted with — including the old 100 — because rotation carries budget and usage over. If your current key is near its limit, contact partners@lmnauto.com to have it raised, or mint a fresh key (trial accounts hold up to 2 active sandbox keys).
  • No API surface change: no new fields, headers, or error codes.

v1.57 — TBD (breaking; coordinate before sandbox rollout)

Dealer filter vocabulary now fails explicitly instead of silently returning zero results. For requests containing source=dealer, unsupported make, model, or sub_model values return 400 validation_error. The response identifies the offending token in error.details.field and error.details.value (and includes error.details.make for model fields). Supported values are case-insensitive. Compatibility aliases observed in partner traffic remain valid, including Mercedes Benz, Lexus + ES350, and Lexus + RX350. This is a contract change from the previous raw-text fallback, which often returned 200 with an empty page. LMN must coordinate rollout with partners before enabling this version in any environment.

v1.55 — TBD (live on sandbox; production ships with the next deploy)

Dealer (encar_*) vehicles under contract are now hidden — reversibly — from vehicle lists, vehicle detail, and Eagle Eye. A dealer listing that goes under contract on Encar (계약중 — a buyer has placed a deposit, but the sale hasn’t finalized) is no longer treated as a normal, purchasable listing:
  • GET /v1/vehicles excludes under-contract dealer rows from list results.
  • GET /v1/vehicles/encar_<id> returns 404 vehicle_not_found for an under-contract car — the same code already returned for a SOLD / WAIT upstream advertisement. See Dealer detail caveats.
  • eagle_eye.match webhooks emit a removals entry with reason: "under_contract" for a dealer car tracked on a static list that goes under contract. On an automatic (filter-based) dealer watch, the same event still removes the car from the match list, but — as before this release — as reason: "delisted"; automatic watches don’t distinguish a contract from a genuine sale. See Removal reasons.
Reversible, unlike every other removal reason or 404 cause on this list. Contracts fall through. When LMN’s next check of that listing finds the contract gone, the vehicle simply reappears — in list results, in detail, and (for a tracked Eagle Eye match, static or automatic) as a new entry in additions on the same eagle_eye.match event, not a new event type. Only static-list matches get the distinct under_contract label on the way out; automatic-watch matches come back the same way but were removed as a plain delisted. Either way, a vehicle your integration was told was gone can legitimately come back — don’t treat a dealer removal as final. Additive / partner-visible behavior change (dealer source only). No fields were added, removed, or renamed — the vehicle_not_found error and the removals / additions shapes are unchanged. Integrations that already handle 404 vehicle_not_found defensively, and that already tolerate unrecognized reason values per conventions, need no changes. If your integration reads reason with a strict switch and no default case, add one now — reason is an open string set, not a closed enum.

Docs — 2026-07-31 (no API change)

Documented: the two event-history endpoints use different delivery_status vocabularies. Long-standing behavior that was never written down. GET /v1/orders/{id}/events returns the raw five values (pending, delivering, delivered, delivery_failed, delivery_skipped); GET /v1/eagle-eye/watches/{watch_id}/events returns a collapsed three (pending, delivered, failed). Nothing changed in either endpoint — both behave exactly as they always have. The consequence worth knowing: on a watch event, failed covers both “your endpoint rejected the delivery” and “no webhook URL was registered” — two situations with opposite remedies. Only the order endpoint distinguishes them. A delivery_status parser shared across the two endpoints will mis-handle one of them. Now stated in webhooks overview (side-by-side comparison), orders, and Eagle Eye.

v1.54 — 2026-07-30

Webhook replay in the partner console — re-send a past event to your endpoint. The order detail page in the partner console now offers Replay on past webhook events, plus a read-only View payload viewer. Replay delivers the real original payload to your registered endpoint, so you can test a receiver fix against the exact bytes that broke it instead of synthetic data. Available on delivered, delivery_failed, and delivery_skipped events; live in both sandbox and production.
A replay reuses the original X-LMN-Event-Id (Stripe “Resend” parity). If your handler deduplicates on event id — as Signing & retries recommends — it will discard the replay before your business logic runs, and nothing will appear to arrive. Clear the id from your dedupe store or bypass the check while testing.
Other behavior worth knowing before your first replay:
  • Fresh X-LMN-Timestamp, re-signed per attempt — verify against the timestamp you receive, exactly as for a first delivery. The body is byte-identical to the original, embedded event_id included.
  • Single attempt, no retry ladder. The console reports your endpoint’s real HTTP status (or a 10s timeout) immediately.
  • Environment is strict — a sandbox replay reaches your sandbox endpoint only, and a production replay your production endpoint. There is no cross-environment path.
  • Rate limit: 5 replays per event per minute.
  • No effect on your API reads — replays are filtered out of GET /v1/orders/{id}/events, and the original event’s own delivery record is never modified, even when a replay fails.
  • No test marker. Unlike the Eagle Eye test webhook (data.is_test: true), a replay is indistinguishable from the original by content — downstream automation will act on it.
Not an API change — no endpoint, request shape, or response field was added or altered. It is listed here because it changes what your endpoint can receive: a second delivery bearing an event_id you have already seen. Full detail: Replaying a webhook.

v1.53 — TBD (live on sandbox; production ships with the next deploy)

Dealer (encar) vehicles on Eagle Eye static lists. Static lists previously accepted auction sources only; dealer favorites had to be approximated with narrow automatic watches. Both tracking surfaces now cover dealer inventory:
  • Static-list creation accepts "dealer" in filters.source — alone or mixed with auction tokens. (source on a static list is declarative; membership was and remains decided per-id.) The former 400 eagle_eye_invalid_filter_for_dealer on this shape is no longer returned.
  • POST …/watches/{watch_id}/matches accepts encar_… ids, resolved against live dealer inventory at add time. Sold/delisted dealer ids land in not_found like any dead id.
  • New add-response key: unavailable — dealer ids LMN could not check this moment (transient upstream failure, or a listing state LMN doesn’t recognize yet). Not a verdict about the car; retry the same id later. Auction ids never produce it, and unavailable ids never count toward the 500-member cap.
  • Hourly refresh for dealer members — any price change (rises included, per the static emission rule) emits eagle_eye.match price_changes; a car that leaves dealer inventory emits removals with reason: "delisted" (auction static members keep auction_complete). A member that can’t be checked is skipped and re-checked next hour — a hiccup never delists a tracked car.
Additive — no existing request shape or response field changed; integrations that ignore unavailable keep working (unchecked ids are simply absent from added). See Static lists and Tracking dealer-source vehicles.

v1.52 — 2026-07-24

POST /v1/orders now enforces the documented listing-price floor on max_bid_amount_usd. The reference has always stated the max bid must be ≥ pricing.listing_price; the check is now actually enforced. An auction order whose max_bid_amount_usd is below the vehicle’s listed price returns 400 missing_max_bid with details.listing_price_usd and details.max_bid_amount_usd. Bids exactly at the listing price pass. Vehicles without a known listing price skip the floor (the > 0 requirement still applies). If your integration intentionally sent sub-listing bids, they were silently accepted before — from this version they are rejected, so read the vehicle’s pricing.listing_price before constructing the order.

v1.51 — 2026-07-22

Order responses now include the saved full vehicle detail snapshot. The Order resource returned by single-order partner API responses now carries a top-level vehicle field with the same VehicleDetail shape as GET /v1/vehicles/{id}. The value comes from the order’s saved creation snapshot, so it is frozen at order creation/backfill time and remains available even if the upstream listing later changes, sells, or disappears.
  • Present on POST /v1/orders, POST /v1/orders/backfill, GET /v1/orders/{id}, PATCH /v1/orders/{id}, DELETE /v1/orders/{id}, POST /v1/orders/{id}/status, POST /v1/orders/{id}/sandbox-status, and POST /v1/orders/{id}/confirm.
  • null for legacy/pre-snapshot orders where no saved vehicle detail exists.
  • GET /v1/orders/{id}/vehicle remains available as a convenience endpoint for the same saved snapshot.
  • List responses and webhook payloads stay compact; use the single-order endpoints when you need the full vehicle detail.
Additive. No fields were renamed or removed. Clients that ignore unknown fields are unaffected.

v1.50 — 2026-07-20

GET /v1/orders/{id}/events now validates limit strictly (1–100). The limit query parameter is now rejected with 400 validation_error when it falls outside the documented 1–100 range, matching the parameter’s long-standing spec. Previously, out-of-range values were handled inconsistently: limit above 100 was silently capped to 100, and limit=0/negative/non-numeric values fell through to undefined behavior. In-range calls are unchanged (same results, same default of 50).
  • Action only needed if you over-request: a call sending e.g. limit=200 previously returned 200 OK with results capped at 100; it now returns 400. Send limit ≤ 100 (the max page size is unchanged).
  • No change to any in-range call, the default, pagination cursors, or the event shape.

v1.49 — 2026-07-15

Order field: secondary_inspection_url — the inspection report link at the decision point. When LMN completes a secondary inspection and moves the order to secondary_inspection_ready, the order now carries the LMN URL of the completed inspection report (secondary_inspection_url, nullable string). The order.status_changed webhook for that transition includes it inside data.order, so you can surface the full report to your dealer right where they confirm (POST /v1/orders/{id}/confirm) or decline (DELETE /v1/orders/{id}). This applies whether the inspection was requested at creation (secondary_inspection_required: true) or LMN chose to inspect voluntarily.
  • Present on the Order resource everywhere it is serialized: GET /v1/orders/{id}, list responses, and all webhook payloads that embed the order.
  • null before the inspection completes (or for orders that never pass through inspection), and for the rare ready-transition where no report URL was recorded.
  • The field is born with the secondary_ prefix — it is not part of the v2.0 rename mapping.
Additive — no request changes, no behavior changes for integrations that ignore unknown fields.

v1.48 — 2026-07-08

Inspection request cutoff for auction orders. Orders with inspection_required: true on an auction vehicle must now be placed by the end of day (auction day − 2) in your integration’s timezone (default Africa/Lagos) — LMN needs time to physically inspect the car before auction day. Worked example: for a Thursday auction you have all of Tuesday, local time. The rule is calendar-based; the auction’s start time is irrelevant.
  • New Vehicle field: inspection_cutoff_at (list + detail) — the pre-computed deadline, serialized in KST offset next to order_cutoff_at. null for dealer/buy-now sources. Read it from the response, don’t recompute.
  • New Order field: inspection_cutoff_at — the deadline that was enforced at creation (audit parity with order_cutoff_at). null unless the order was created with inspection_required: true on an auction vehicle, and null for orders predating this version.
  • New error: 409 past_inspection_cutoff on POST /v1/orders (and /v1/orders/backfill) — returned instead of creating the order when the deadline has passed. details carry UTC inspection_cutoff_at + now. The same order remains placeable with inspection_required: false until order_cutoff_at.
Additive except for the new 409: integrations that send inspection_required: true close to auction day must now handle past_inspection_cutoff (recommended fallback: re-offer the order without inspection). Orders without the flag, dealer orders, and all other endpoints are unaffected. Sandbox enforces the identical rule, so the 409 is reproducible there. See Inspection request cutoff.

v1.47 — 2026-07-06

New filter: mileage_min_km on GET /v1/vehicles. Inclusive lower mileage bound in kilometers, the counterpart to the existing mileage_max_km. Applies to all sources, including source=dealer (live retail inventory). Additive. No fields renamed or removed. Requests without the param behave exactly as before.

v1.46 — 2026-07-06

Webhooks for inspection decisions. Both partner decisions at the inspection_ready decision point now fire order.status_changed, in addition to the synchronous API response:
  • POST /v1/orders/{id}/confirm — enqueues order.status_changed (previous_status: inspection_ready, order.status: acquiring).
  • DELETE /v1/orders/{id} from inspection_ready — enqueues order.status_changed (previous_status: inspection_ready, order.status: cancelled, cancellation_reason: inspection_declined).
Additive. No fields renamed or removed, and no existing webhook payload shape changed — this only adds new deliveries for two transitions that previously emitted nothing. placed → cancelled (partner DELETE) and the partner status pushes (in_transit → customs, customs → delivered) are unaffected and remain silent. Why: the partner’s webhook pipe should carry every outcome of the inspection_ready decision point, not just the admin-driven half — see order.status_changed and Order firing rules.

v1.45 — 2026-07-06

Eagle Eye: static lists, and PUT/DELETE removed from the watches surface.

⚠️ Breaking: PUT and DELETE /v1/eagle-eye/watches/{watch_id} removed

Both methods now return 405. GET /v1/eagle-eye/watches/{watch_id} is unaffected.
  • No replacement for editing filters. There is no partner-facing way to rename a watch or change its filters / signals / flexibility after creation. Create a new watch with the desired configuration and pause the old one.
  • No replacement for deleting. Pause the watch instead (POST …/pause, unchanged). A paused watch stops counting against the 100-active-watch cap and can be resumed at any time.
  • Why: static lists (below) are built on the same watch resource. A stray DELETE would hard-delete a partner’s hand-picked car list along with its match rows, and PUT is a full filter replace — both are too destructive for a single call on what can now be a curated list, not just a saved search.
  • eagle_eye_watch_limit (429, code unchanged) — message reworded to say “pause an existing watch first” instead of referencing delete.

New: static lists

A watch created with automatic_list_update: false is a static list — you manage its cars explicitly instead of LMN deriving membership from filters.
  • automatic_list_update (boolean, optional, default true) — new field on POST /v1/eagle-eye/watches, echoed on every watch response. false restricts filters to source (auction sources only — glovis/sk/aj/lotte/kcar; dealer is rejected with 400 eagle_eye_invalid_filter_for_dealer), rejects signals / flexibility / any other filter field with 400 validation_error, and skips seeding — the watch is born empty.
  • New endpoint POST /v1/eagle-eye/watches/{watch_id}/matches — add specific vehicle_ids to a static list. Response partitions added / already_in_list / not_found. A live listing with no price yet is added with price_krw: null — that’s the intended use case, not an error.
  • New endpoint POST /v1/eagle-eye/watches/{watch_id}/matches/remove — hard-remove specific vehicle_ids (unlike /matches/hide, this stops monitoring the car entirely). Response partitions removed / not_in_list.
  • New error code eagle_eye_watch_not_static (400) — both endpoints above 400 when called on an automatic watch.
  • The existing error code eagle_eye_match_batch_too_large (400) also applies to both new endpoints — same per-call cap of 30 vehicle_ids as /matches/hide / /matches/unhide.
  • Membership rules: a 500-member per-list cap counts hidden members too. Re-adding a car already in the list — hidden or not — answers already_in_list and does not change its hidden state; /matches/unhide is the only un-hide affordance.
  • Static-list members report every price change, including a price appearing on a previously un-priced listing (NULL→value) and price rises — unlike automatic watches, which stay drop-only. Entries carry match_reason: ["manual"] instead of filters / signals.*. See webhook event types — static lists.
  • No webhook is sent for the add/remove calls themselves — only for subsequent price changes LMN observes on a member.
See Eagle Eye endpoints — Static lists for request/response examples, the favorites recipe, and integration guidance.

v1.44 — 2026-07-02

Secondary inspection workflow. Three API-visible changes closing the loop between requesting a pre-purchase inspection and deciding what happens after it completes.
  • inspection_required on order creation. POST /v1/orders accepts an optional inspection_required boolean (default false). true requests a pre-purchase secondary inspection: LMN routes the order placed → inspection_in_progress → inspection_ready instead of straight to acquiring. Immutable after creation — every Order response, and every webhook payload via the shared response mapper, now carries inspection_required. Replaying the same Idempotency-Key + vehicle_id with a different inspection_required than the original request now returns 422 idempotency_key_reused with details.original_inspection_required.
  • New endpoint: POST /v1/orders/{id}/confirm. Your decision at inspection_ready to proceed to purchase. Transitions the order to acquiring. No request body. Allowed only while status = inspection_ready — any other status (including a retry after success) returns 409 invalid_status_transition with details.current_status; an unknown or foreign order ID returns 404 order_not_found. No webhook is emitted — the updated Order in the response is the notification, same as the existing DELETE.
  • DELETE /v1/orders/{id} — second cancel window. Cancellation is now also allowed from inspection_ready, in addition to placed. From inspection_ready the response carries cancellation_reason: inspection_declined (unchanged: dealer_cancelled from placed). Cancelling while inspection_in_progress remains rejected — the decision point is inspection_ready only.
Additive. No existing fields renamed or removed. Admin-side inspection statuses (inspection_in_progress, inspection_ready) and their transitions shipped earlier (#219) — this release adds the partner-facing surface around them: requesting an inspection at creation, and deciding what happens once it’s ready.

inspection-ui v0.2.0 — 2026-06-29

Client rendering library (@lmnauto/inspection-ui) — not an API contract change; the API contract is unchanged. The inspection UI is now fully themeable. Pass a typed theme prop (or set --lmn-insp-* CSS variables) to match your brand: accent, severity, accident-badge, and damage-marker colors, plus surfaces, card radius, and font. Setting accent retints the brand-red family (bad-severity text, accident “bad” badge, CHANGE marker) in one go. Defaults are unchanged, so existing integrations render identically. See Inspection rendering → Theming.

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 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. 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): 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 and Eagle Eye overview — 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).

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).

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-Benz1800 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),dealerfee(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 automaticallyOrder.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,oceanfreight300, 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-dependentbid_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_usdpricing.listing_price
    • top-level sold_price_usdpricing.sold_price
    • pricing.breakdown_usdpricing.breakdown
    • pricing.history[].price_usdpricing.history[].price
    • pricing.similar_sales[]comparables.past_sales[]
    • pricing.similar_upcoming[]comparables.upcoming_sales[]
    • pricing.market_assessment.market_median_usdpricing.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.
  • 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.
  • 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//events.
  • 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/ — 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.
  • 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.
  • 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_foundvehicle_not_found / order_not_found. unauthorizedmissing_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_lagoseta_destination with destination_port. Removed inspection_fee from Order amounts. Simplified cancellation to proceed-only via DELETE.