Skip to main content
Eagle Eye is LMN’s saved-condition and alerting surface. It lets a partner preview a buying condition, save it as a watch, review the current match list, and receive webhook batches when new opportunities or changes appear. For product context, see the partner-facing overview: Eagle Eye overview.

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 webhook additions / 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:
See webhook event types for the full payload shape, and webhook signing & retries for delivery behavior. The event is batched and can include:
  • 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 matches include 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.
The vehicle must currently be on the watch. The event rides the real delivery pipeline — same outbox, same cron, same 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 same vehicle_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 on filters (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

Pass automatic_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.
A static list still counts against the 100-active-watch cap and supports the same /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

Each id is resolved live at add time: auction ids against live auction inventory, 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/hide to stop showing a car on an automatic watch instead.
  • 400 eagle_eye_match_batch_too_largevehicle_ids exceeds the per-call cap of 30. Paginate GET …/matches and call again in batches.
  • 400 validation_error — adding the resolved cars would exceed the 500-member per-list cap (unavailable ids don’t count). See membership rules below.
  • 404 eagle_eye_watch_not_found — unknown or cross-tenant watch_id.
No webhook is sent for an add — you performed the action yourself.

Removing cars — POST /v1/eagle-eye/watches/{watch_id}/matches/remove

Removal hard-deletes the match row — the car stops being monitored entirely and 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:
  1. A dealer favorites a car in your app that’s currently published on a KR auction source without a price.
  2. You add it to a static list: POST /v1/eagle-eye/watches/{watch_id}/matches with the car’s vehicle_id. It’s added with price_krw: null.
  3. When the auction source later publishes a price for that car, LMN’s next ingest pass detects the change and sends eagle_eye.match with the car in price_changesprevious.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).
  4. Show the dealer the price and let them act via POST /v1/orders as usual.
The same recipe covers dealer favorites: a dealer favorites an encar car in your app, you add its 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 in price_changes, a car that leaves dealer inventory arrives in removals with reason: "delisted", and a car that goes under contract on Encar arrives in removals with reason: "under_contract" — reversible: if the contract falls through, LMN’s next hourly check restores it as a new additions entry, 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" in filters.source (make is required — 400 eagle_eye_filters_required without 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 as reason: "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.
Source-specific behavior at a glance: 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 the filters 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_usd tolerance (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.
If both a damage boolean (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 a level 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 carries match_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_reasonmatch_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 }
Numeric flex example — landed cost just above the strict limit:
Categorical flex example — alternative model admitted:
An exact match returns "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:
Exact-match webhook entries omit the 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.

Eagle Eye error codes