Skip to main content
This guide is for a storefront that shows auction cars and dealer cars on one page. It adds recipes and rendering rules on top of the endpoint contract; every behaviour it relies on is defined in Vehicles and Orders, and those pages win if anything here is ever out of date.
Building with Lovable, Cursor or Claude? Connect the documentation connector first and paste the combined storefront prompt from that page — it encodes every rule below.

Source vs. purchase method

Partners regularly conflate two different axes. Keep them separate in your data model and your UI: Two rules follow:
  • A “Buy now” toggle filters on purchase method, not on source. Dealer cars and no_bid auction cars are both Buy now. Do not make dealer cars appear only when Buy now is on — they belong in “All sources” either way.
  • “All sources” means two requests, not one request with source omitted. Omitting source returns auction listings only (see below).

Before you start

  • Base URL and key: Quickstart. All examples use sandbox (https://sandbox-api.lmnauto.com, lmn_stg_* keys); production is https://api.lmnauto.com with an lmn_prd_* key and an IP allowlist.
  • Calls are server-side only — the key lives in your backend (LMN_API_KEY), the browser talks to your backend. See Build with AI for the rule and why.
  • Every example below was executed against the live API before publishing. Values such as counts and IDs are illustrative; shapes and status codes are exact.

Step 1 — Load the catalog, never guess names

make, model and sub_model must be spelled as LMN’s catalog spells them. Pull the catalog once a day and build your pickers from it. Two pulls: without source the response describes auction houses and dealer together (the make/model pickers for a combined page); with source=dealer it adds the dealer-only sub_models:
  • makes / models are the values make and model accept across the sources in described_sources — build pickers from the no-source pull so auction-only makes and models are not missing. sub_models (from the source=dealer pull) are the exact values sub_model accepts — currently for source=dealer only (see filter partition).
  • Facets are vocabulary, not stock. A listed value may have no car today, and dealer stock is never observed in availability. Use facets to pick accepted values; only a list request tells you whether cars exist. Details: GET /v1/vehicles/facets.
  • On the dealer request a guessed make, model or sub_model returns 400 validation_error with details.field and details.value. Auction houses accept any pair — a typo there just returns an empty page — which is exactly why pickers must come from facets rather than free text.

Step 2 — Three request shapes

Auction only

Works with or without make. Omitting source entirely is equivalent for the rows you get back (auction only), but naming the houses keeps the request self-documenting and your whitelist explicit.

Dealer only

  • make is required (400 validation_error, details.field: "make" without it). CSV is allowed: make=Hyundai,Kia fans out to one upstream search per make.
  • The dealer upstream caps a page at 48 rows regardless of limit; above that the response carries X-LMN-Dealer-Page-Max: 48.
  • Every dealer response includes source_status for encar and danawa — read it before trusting the rows (see Failures vs. empty results).

Combined page

Issue the two requests above in parallel and merge on your side. A single mixed request (source=glovis,dealer) is supported for the first page only — the moment you pass cursor with a mixed source you get 400 mixed_source_pagination_unsupported — so a page that paginates must use separate requests from the start. Contract: Mixed source — first page only. A backend function (Node 18+, any serverless runtime) that does this — each source fails, pages and ends independently:
Each side of the result carries its own state (ok / error / exhausted / skipped / held), rows, nextCursor and, for errors, status, code, message and requestId — exactly what the failure state must show. Feed each side’s nextCursor straight back as the next call’s cursors.auction / cursors.dealer: a string continues, undefined fetches page 1, and null marks that source exhausted and stops its requests while the other keeps paging. An error result returns the cursor it was sent, so the same page is retried — a failed page is never mistaken for an exhausted source. To retry one source only (the dealer side after a 503, say), pass { only: 'dealer' }: the dealer request is re-issued with its returned cursor, no auction request is made, and the auction side comes back as state: 'held' with the cursor you passed in unchanged — keep the auction rows you already rendered and only replace the dealer section. The frontend receives { auction, dealer }, each with its own state, rows and nextCursor, and renders them as described in Rendering.

Which filters go to which request

Build each request from a whitelist. Forwarding the auction query to the dealer request (or vice versa) is the single most common cause of missing dealer listings. When you filter on an auction-only field with source omitted, the API silently excludes dealer rows and (when make is present and the dealer upstream is reachable) adds X-LMN-Filtered-Out: dealer=N. That header is a best-effort hidden-count estimate for an auction-only request — the dealer upstream is queried to compute it, but no dealer rows are returned. Contract: Null-strict semantics.

Pagination

  • One cursor per source. Store next_cursor from the auction response and from the dealer response separately; send each back only to the request that produced it. next_cursor: null means that source is exhausted — record that and stop requesting it while the other source keeps paging (re-requesting page 1 of an exhausted source wastes trial budget and re-yields rows you already deduped). Keep three states per source: not fetched yet, continuing from a cursor, exhausted.
  • Never pass cursor with a mixed source. 400 mixed_source_pagination_unsupported.
  • Reset both cursors on any filter or sort change. Cursors encode a position in a specific result set. Reusing one after the filters change does not reliably fail — it can return page N of the new result set, silently skipping its first page. Treat “user changed anything” as “start both sources from page 1”.
  • Dealer pages are at most 48 rows. If you ask for more, the dealer half is capped and X-LMN-Dealer-Page-Max: 48 tells you so; keep following the dealer cursor.
  • Dedupe by id. Auction IDs (glovis_…, sk_…, …) and dealer IDs (encar_…) never collide, but a re-render that fires the same request twice will. Key your list by id.
  • Label every card with its source. row.source is on every row; show it.

Sorting across sources

sort sorts each request’s result set. Two separately fetched pages cannot be globally sorted unless you have loaded all pages of both, so:
  • Either interleave by source (auction block, dealer block, or alternating) and say so in the UI, or
  • Sort within the loaded window and label it (“sorted by price on this page”).
Do not label a merged list “sorted by price” across sources unless you have actually loaded everything. Note that the default sort=auction_date_asc has no meaning for dealer rows (no auction date); on the dealer request prefer price_asc, price_desc, year_desc or mileage_asc. Sort values: Sort.

Rendering cards

Any missing value renders as “Unknown”. A 0 price or a blank year is a bug on the storefront, not data.

Failures vs. empty results

A storefront that shows “No vehicles found” for a 401, a 400, or a thrown fetch hides the exact information that fixes it. Separate the three states in code and in the UI: Read source_status on every dealer response: fresh is live; stale is a cached page up to 30 minutes old (show as_of); partial means some of a multi-make query answered; unavailable means none did. A dealer-only request returns 503 dealer_upstream_unavailable only when every dealer source is down. Contract: Dealer upstream unavailability. Per-symptom fixes: Missing dealer listings.

Integration acceptance checklist

“Vehicles render” is not a completion criterion. Work through each line with a reproducible filter (BMW is a good one: both sources usually have stock) and keep the actual responses as evidence.
  • Auction only: source=glovis,sk,aj,lotte,kcar&make=BMW&limit=24 returns 200 with ≥1 row and next_cursor.
  • Dealer only: source=dealer&make=BMW&limit=24 returns 200, source_status.encar.status is fresh or stale, and ≥1 row. (partial/unavailable is a retry-later, not a failed integration.)
  • Combined page issues two requests — confirm in your backend logs or network tab that both were sent, in parallel.
  • Page 2 per source: the auction cursor fetches auction page 2; the dealer cursor fetches dealer page 2; neither cursor is ever sent with a mixed source.
  • Filter change resets both cursors — change model and confirm both requests go out without cursor.
  • Missing-make UX: with no make selected, the dealer request is not sent and the page shows the “select a make to include dealer listings” affordance — not an empty grid, not “No vehicles found”.
  • Failure state: temporarily use a wrong key. The page must show the 401 invalid_api_key error with a request_id, not “No vehicles found”. Restore the key.
  • Labels: every card shows its source, the listing price labelled as such, the estimated landed cost labelled as an estimate, and USD.
  • Unknowns: a row with trim: null renders “Unknown”.
  • No key in the frontend: search the built client bundle for lmn_stg_ and lmn_prd_ — zero hits.
Zero dealer rows is not a failed check when source_status.encar is healthy — a narrow filter can legitimately match nothing. Broaden (drop model, widen year or price, or try a high-volume make such as Hyundai or Kia) until you get a positive dealer result once; then re-narrow.