API surfaces
PUT and DELETE /v1/eagle-eye/watches/{watch_id} were removed from the partner surface — see the changelog for details. There is no partner-facing way to rename a watch or edit its filters; create a new watch with the desired configuration and pause the old one.
Schema reuse
Eagle Eye reuses the existing Open API vehicle model. Search results and webhookadditions / price_changes embed the same VehicleSummary shape returned by GET /v1/vehicles; Eagle Eye metadata such as watch_id, match_reason, and signal_detail wraps that vehicle object instead of replacing it.
Match rows include a vehicle snapshot with that same VehicleSummary shape when LMN has captured one for the match. For older or not-yet-refreshed rows, vehicle can be null; use vehicle_id with GET /v1/vehicles/{id} as the fallback.
The vehicle_id in a match is the same ID used by GET /v1/vehicles/{id} and the existing order API.
Webhook
Eagle Eye emits one webhook type:additions— new vehicles that matched the watch,price_changes— watched vehicles whose price dropped enough to notify,removals— vehicles that disappeared or no longer qualify.
signal_detail so the partner can explain the alert:
price_drop is drop-only. If a vehicle price increases while still qualifying, the current match updates silently and loses the stale signals.price_drop reason. If the increase makes it fail the watch’s filters, such as rising above price_max_krw, LMN sends a removal instead.
Webhook delivery uses the same HMAC signing and retry behavior as order webhooks.
Testing your receiver — POST /v1/eagle-eye/watches/{watch_id}/test-webhook
Sandbox only. Sandbox has no automatic Eagle Eye evaluators, so this endpoint
is the way to make sandbox emit an eagle_eye.match webhook on demand — it is the
primary integration-testing mechanism, not a convenience.
X-LMN-Signature/X-LMN-Timestamp/X-LMN-Event-Id
headers, same retries — so it exercises exactly what production delivery exercises.
The payload is a normal eagle_eye.match with one additions entry (the stored
vehicle summary) plus data.is_test: true. Always check is_test before
letting downstream automation act on an addition — it is present on every test
event and never on organic ones.
Responses:
Test fires are allowed on muted and paused watches (an explicit test is user
intent; mute/pause gates only automatic evaluation). The same action is available
per-vehicle in the partner console’s sandbox view.
Acting on a match
Eagle Eye does not create orders directly. A match carries the samevehicle_id used by the existing order API.
When the dealer wants to act, send that vehicle_id to POST /v1/orders.
Static lists
Most watches are automatic: LMN adds and removes cars for you based onfilters (and optionally signals). A static list flips that model — you control membership car-by-car, and LMN only monitors the cars you’ve added; it never adds or removes any on its own.
Static lists exist for one core use case: a dealer favorites a specific car, LMN watches it, and you get a webhook the moment its price is set or changes — including a car that had no price yet at all (“first-price-set”), a transition automatic watches deliberately never report (see eagle_eye.match for why).
Static lists accept both auction and dealer (encar) vehicles. Membership routing is per-id: auction ids (
glovis_…, sk_…, …) resolve against live auction inventory; encar_… ids resolve against live dealer inventory. One list can mix both. Per-source behavior — refresh trigger, removal reason, error surfaces — is summarized in Tracking dealer-source vehicles.Creating a static list
Passautomatic_list_update: false when creating the watch:
boolean
default:"true"
true (default, or omitted) — LMN adds and removes cars automatically based on filters / signals / flexibility, exactly as documented elsewhere on this page.false — creates a static list. Only filters.source is honored, and every source token is accepted — auction (glovis, sk, aj, lotte, kcar) and dealer alike. source is declarative on a static list: membership is decided per-id at add time, not gated by it. Every other filters key, plus signals and flexibility, returns 400 validation_error naming the offending field — inert config is rejected rather than silently stored. filters.make / filters.model are not required on a static list. The watch is born empty: nothing is seeded, active_match_count starts at 0./mute, /unmute, /pause, /resume controls and GET …/matches as an automatic watch. POST …/matches/hide / /matches/unhide also work the same way — hiding is a read-side mute (a hidden car keeps refreshing its price silently but stops appearing in the default match list and stops notifying); it is not the same as removing a car (below). Only the membership mechanism — how cars enter and leave the list — differs.
Adding cars — POST /v1/eagle-eye/watches/{watch_id}/matches
encar_… ids against live dealer inventory. A live auction listing that has no price yet resolves normally — price_krw: null in added — because tracking an un-priced car until it gets a price is exactly what a static list is for (dealer listings always carry a price). An id that isn’t currently live (typo, delisted, sold — either source) lands in not_found; a dealer (encar_…) id currently under contract on Encar lands in not_found too — it isn’t a dead id, so add it again once the contract clears. An unresolvable id is never an error for the batch.
unavailable is the third bucket, for dealer ids only: LMN could not check the car this moment — a transient upstream failure, or a listing state LMN doesn’t recognize yet. Unlike not_found, it is not a verdict about the car — retry the same id later. Auction ids never land here.
400 eagle_eye_watch_not_static— the watch is an automatic watch (automatic_list_update: true). Cars can only be added to static lists; use/matches/hideto stop showing a car on an automatic watch instead.400 eagle_eye_match_batch_too_large—vehicle_idsexceeds the per-call cap of 30. PaginateGET …/matchesand call again in batches.400 validation_error— adding the resolved cars would exceed the 500-member per-list cap (unavailableids don’t count). See membership rules below.404 eagle_eye_watch_not_found— unknown or cross-tenantwatch_id.
Removing cars — POST /v1/eagle-eye/watches/{watch_id}/matches/remove
active_match_count decrements. This is different from hiding: POST …/matches/hide mutes a car (it keeps refreshing its price silently and can be restored with /matches/unhide), while removal is permanent — to watch the same car again you’d add it back and it starts over with a fresh first_matched_at.
Same static-only gate (400 eagle_eye_watch_not_static) and per-call cap (400 eagle_eye_match_batch_too_large, 30) as adding. No webhook is sent for a remove.
Membership rules
The 500-member list cap counts hidden members too. A static list’s size is checked against total membership, not just visible/active rows — hidden entries still occupy a slot because they keep refreshing silently in the background and can be unhidden at any time. To reclaim space, unhide-then-decide or remove cars you no longer want monitored at all; adding more members never bumps a hidden one out automatically.
Re-adding a car that’s already in the list — hidden or not — answers
already_in_list and does not change its hidden state. Adding is not an un-hide affordance. POST …/matches/unhide is the only way to restore a hidden car to the default match-list view.The favorites recipe
The motivating use case for static lists is un-priced auction listings:- A dealer favorites a car in your app that’s currently published on a KR auction source without a price.
- You add it to a static list:
POST /v1/eagle-eye/watches/{watch_id}/matcheswith the car’svehicle_id. It’s added withprice_krw: null. - When the auction source later publishes a price for that car, LMN’s next ingest pass detects the change and sends
eagle_eye.matchwith the car inprice_changes—previous.price_krw: null, the new price populated,match_reason: ["manual"]. See static-list emission for the full rule, including why price rises also notify here (unlike automatic watches). - Show the dealer the price and let them act via
POST /v1/ordersas usual.
encar_… id to the list, and LMN notifies on every subsequent price change and on delisting. The only difference is that dealer listings already carry a price at add time, so the flow starts at “watch for changes” instead of “wait for the first price”.
Integration guidance
- One shared static list per environment is enough for most integrations. Watches are partner-scoped, not dealer-scoped — eagle-eye tracks the cars, not who favorited them — so car→dealer mapping stays on your side. Per-dealer watch scoping is not available in v1.
- Static lists count against the same 100-active-watch cap as automatic watches (per partner, per environment). A handful of static lists (e.g. one per environment) leaves ample room for automatic watches.
- To retire a static list, pause it (
POST …/pause) rather than removing every car —PUT/DELETE /v1/eagle-eye/watches/{watch_id}no longer exist for partners.
Tracking dealer-source vehicles
Dealer (encar) vehicles are supported on both tracking surfaces:- Static lists — add specific dealer cars by id (
encar_…) exactly like auction cars. LMN re-checks each dealer member against live dealer inventory hourly: any price change (drops and rises, per the static emission rule) arrives inprice_changes, a car that leaves dealer inventory arrives inremovalswithreason: "delisted", and a car that goes under contract on Encar arrives inremovalswithreason: "under_contract"— reversible: if the contract falls through, LMN’s next hourly check restores it as a newadditionsentry, not a new event type. If a member can’t be checked on a given pass (transient upstream failure), it is skipped and re-checked next hour — a hiccup never delists a tracked car. - Automatic watches — include
"dealer"infilters.source(makeis required —400 eagle_eye_filters_requiredwithout it) and LMN polls dealer inventory for the filters hourly: new matches, price drops after a baseline, removals. A dealer car that goes under contract is removed here too, but reported asreason: "delisted"— automatic watches don’t distinguish a contract from a genuine sale (see Removal reasons). It can still return as a plain new match once it’s purchasable again.
Mixed lists and mixed watches (
source: ["dealer", "glovis", …]) are both supported. On a mixed static list the two member groups refresh on their own triggers (auction: per scrape batch; dealer: hourly) but share the same watch, match list, and webhook stream — no coordination needed on your side. For filter semantics that differ by source, see Damage filter semantics.
If you prefer criteria-based tracking of a dealer segment rather than hand-picking cars, a narrowly scoped automatic watch remains a good pattern:
Filter reference
New filter fields (search and watch bodies)
The following fields are accepted in thefilters block of POST /v1/eagle-eye/search and POST /v1/eagle-eye/watches:
string
Single lowercase fuel token. Accepted values:
gasoline, diesel, lpg, hybrid, gasoline_hybrid, diesel_hybrid, electric, plug_in_hybrid, hydrogen.Supply exactly one token — an array or an unrecognized token returns 400 validation_error. For multiple fuels, use fuel_in instead.string[]
Multiple fuel tokens, OR-ed. Same accepted values as
fuel; must be non-empty when present. Fuel is applied as a post-filter over fetched listings on every source, so OR-ing multiple fuels has no upstream cost.boolean
true filters to clean cars only. false or absent applies no filter.Per-source semantics differ — see the damage filter semantics section below before using this field across mixed sources. For frame-sensitive buyers, no_frame_damage is the source-equivalent alternative.boolean
true excludes cars with structural frame damage; exterior panel repair is permitted. false or absent applies no filter.This filter is source-equivalent — it maps to the same structural-integrity standard on both dealer and auction sources. Recommended for buyers who care about frame integrity but accept repaired exterior panels.number
Maximum estimated landed cost in USD (positive number). Computed per-vehicle using your partner’s FX rate and freight configuration.Two implications:
- Null exclusion: vehicles whose landed cost cannot be computed are excluded when this filter is active.
- FX drift: a pure exchange-rate move (no listing price change) can admit or remove vehicles from an active watch. Set your bound with a buffer for normal FX volatility, or use the
flexibility.fields.estimated_landed_max_usdtolerance (see flexibility).
Damage filter semantics
no_accident and no_frame_damage map to different underlying predicates depending on inventory source:
D7 asymmetry:
no_accident is not semantically equivalent across sources. On dealer it means zero exterior repairs; on auction it means no structural exchange (exterior work is permitted because grade A is the strongest clean-accident classification auction sources express). no_frame_damage is source-equivalent and is the recommended filter for buyers who need a consistent structural-integrity guarantee across both dealer and auction inventory.
Approximately 3% of dealer stock lacks a valid vehicle inspection record (성능기록부). When either damage filter is active, those cars are excluded — unknown condition is treated conservatively as not qualifying. This exclusion is not surfaced as a separate indicator in the response.
no_accident or no_frame_damage) and accident_grade_in are present in the same request, the engine intersects the two. A contradictory combination — for example no_accident: true with accident_grade_in: ["B"] — yields zero matches rather than a 400 error.
Buyer-fit tolerance (flexibility)
flexibility is an optional top-level block in the search and watch request body. It widens numeric filter bounds and expands categorical filter alternatives so a watch catches cars that fall just outside its strict criteria. When absent, all filters are applied strictly.
Flex matches are tagged match_type: "flex" in every response surface (search rows, match list items, and webhook entries). Strict matches carry match_type: "exact".
Request shape
FlexLevel
Applies to all numeric and year filter fields that are present in the request and not overridden by
flexibility.fields. Does not apply to categorical fields (model, trim, fuel). Accepted values: none | small | medium | large. Default none.object
Per-field overrides. Each key must reference a filter field that is present in the same request — a key referencing an absent filter returns
400 eagle_eye_invalid_flexibility.FlexLevel
required
Tolerance level for this field.
none | small | medium | large.string[]
For categorical fields (
model, trim, fuel) only. Each value creates one additional query variant. Required when level != 'none' on a categorical field. Maximum 4 values per field.Numeric and year tolerance
These fields accept alevel tolerance: year_min, year_max, mileage_max_km, price_max_krw, estimated_landed_max_usd, auction_count_min, auction_count_max.
Year fields always use the slack column. Non-year max bounds apply
floor(value × (1 + tolerance)); min bounds apply ceil(value × (1 − tolerance)).
Santa Fe example — strict estimated_landed_max_usd: 13700 with level: "medium":
- Effective bound:
floor(13700 × 1.05) = 14385 - A vehicle priced at $13,782 is admitted and tagged
match_type: "flex" - A vehicle priced at $13,200 is admitted and tagged
match_type: "exact"
Categorical tolerance
Categorical fields (model, trim, fuel) do not have a numeric percentage tolerance. To widen them, supply allowed_values — each value creates one additional search variant that runs alongside the strict base query.
level must be none (explicit no-op) or paired with non-empty allowed_values. Supplying level: "small" (or any non-none level) without allowed_values on a categorical field returns 400 eagle_eye_invalid_flexibility.
Non-flexable fields
no_accident, no_frame_damage, source, and make only accept level: "none". This serves as an explicit per-field override of flexibility.default — useful to pin a boolean outside the default without setting a tolerance. Passing any other level for these fields returns 400 eagle_eye_invalid_flexibility.
Caps
Exceeding either cap returns
400 eagle_eye_invalid_flexibility with details.field naming the offending field.
Response metadata — match_type and flex_detail
Every match-bearing surface now carriesmatch_type, plus flex_detail describing any applied tolerance — surfaces differ on whether an exact match omits flex_detail or includes it as null, so see each surface’s example below for its exact-match shape. These fields are orthogonal to match_reason — match_reason tells you why a vehicle is in the watch (filters / signals); match_type / flex_detail tell you how closely it matched.
Search row
"exact" | "flex"
required
"exact" when all filters matched strictly. "flex" when at least one filter matched only within its tolerance.object
Present only when
match_type is "flex". One key per field that required the tolerance. Each value is either a numeric entry or a categorical entry:Numeric entry: { requested: number, level: string, effective: number, actual: number }Categorical entry: { requested: string, allowed_values: string[], actual: string }"match_type": "exact" with no flex_detail key.
Match list item
GET /v1/eagle-eye/watches/{watch_id}/matches now includes match_type and flex_detail on every row. Unlike search rows, match list items always include the flex_detail key; it is null for exact matches.
Webhook eagle_eye.match
additions and price_changes entries in the eagle_eye.match webhook now carry match_type and flex_detail:
flex_detail key (same omission behavior as search rows). removals entries are not changed — they carry only vehicle_id and reason.
Watch response
The watch resource (GET/POST /v1/eagle-eye/watches) now echoes back the stored flexibility configuration:
flexibility is null when not configured. There is no partner-facing update path for flexibility (or any other filter/signal config) after creation — PUT /v1/eagle-eye/watches/{watch_id} has been removed (see the changelog). Create a new watch with the desired configuration and pause the old one.