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_bidauction 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
sourceomitted. Omittingsourcereturns 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 ishttps://api.lmnauto.comwith anlmn_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/modelsare the valuesmakeandmodelaccept across the sources indescribed_sources— build pickers from the no-sourcepull so auction-only makes and models are not missing.sub_models(from thesource=dealerpull) are the exact valuessub_modelaccepts — currently forsource=dealeronly (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,modelorsub_modelreturns400 validation_errorwithdetails.fieldanddetails.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
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
makeis required (400 validation_error,details.field: "make"without it). CSV is allowed:make=Hyundai,Kiafans out to one upstream search per make.- The dealer upstream caps a page at 48 rows regardless of
limit; above that the response carriesX-LMN-Dealer-Page-Max: 48. - Every dealer response includes
source_statusforencaranddanawa— 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:
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_cursorfrom the auction response and from the dealer response separately; send each back only to the request that produced it.next_cursor: nullmeans 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
cursorwith a mixedsource.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: 48tells 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 byid. - Label every card with its source.
row.sourceis 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”).
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 a401, 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=24returns200with ≥1 row andnext_cursor. - Dealer only:
source=dealer&make=BMW&limit=24returns200,source_status.encar.statusisfreshorstale, and ≥1 row. (partial/unavailableis 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
modeland confirm both requests go out withoutcursor. - 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_keyerror with arequest_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: nullrenders “Unknown”. - No key in the frontend: search the built client bundle for
lmn_stg_andlmn_prd_— zero hits.
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.