# TheCarApi — full content
Generated 2026-08-24. Canonical site: https://thecarapi.com
---
# SEO for inventory-driven sites: what changes when AI crawlers cannot run JavaScript
URL: https://thecarapi.com/blog/seo-for-data-driven-sites
Published: 2026-08-05 · Updated: 2026-08-07
Category: Guides
Google renders JavaScript, eventually. GPTBot, ClaudeBot and PerplexityBot do not render it at all — which changes the calculation for any site whose content is its inventory.
Inventory-driven sites — vehicle marketplaces, property portals, anything where the catalogue is the content — get most of their traffic from organic search. They are also, disproportionately, built as client-rendered single-page applications, because that is what the frontend tooling makes easy.
Those two facts are in direct conflict, and the conflict got sharper when AI crawlers became a meaningful discovery channel. This is what the problem actually looks like and what to do about it.
## What a crawler sees
A client-rendered SPA serves every URL the same HTML document:
```html
Cars — Marketplace
```
Identical for the homepage, for every category page, and for all 200,000 listing URLs. Same title, no description, no canonical, no content.
Google will render the JavaScript and eventually see the real page. But rendering happens in a second pass, on a separate queue, and crawl budget is allocated from the first. What Googlebot sees on pass one is a large number of URLs with an identical title and no text — which is, structurally, the profile of a low-quality auto-generated site. On a site whose whole proposition rests on being trusted, that is a bad first impression to make at scale.
> **AI crawlers do not render at all** — The crawlers behind AI answers and citations generally fetch HTML and read it. They do not execute your bundle. A client-rendered site is not partially visible to them — it is a blank page. If being the source an AI answer cites has any value to you, that is the whole channel gone.
## The fix, in order of impact
### 1. Real URLs and real anchors
Prerequisite for everything else, and frequently the missing piece. If navigation is ``, there is nothing to crawl and nothing to link to — no amount of meta-tag work helps a site with one URL.
Every page needs its own path, and every navigation control needs to be an `` that a crawler can follow and a user can middle-click. Intercept the click for client-side routing if you want the SPA feel; keep the anchor.
### 2. Prerender the pages you can enumerate
Marketing pages, documentation, articles, category pages — anything with a known, bounded URL list — should be rendered to static HTML at build time. For a React app this is not a rewrite: render each route with `renderToString`, inject the result and the per-page head into the built HTML template, and hydrate on the client.
You get complete HTML for crawlers, a faster first paint for users, and no framework migration.
### 3. Server-render the head for pages you cannot enumerate
Individual listing pages are the hard case: there can be hundreds of thousands, they change constantly, and prerendering all of them at build time is not viable. The pragmatic answer is to inject the head server-side per request — title, description, canonical, OG tags and structured data — from a single database lookup, while leaving the body to the client.
It is perhaps sixty lines of string replacement on the HTML template, and it converts a mass of identical shells into individually described pages.
```html
2021 Kia Niro 1.6 GDi HEV, 77,205 km — auction lot | Example
```
_Generated from fields you already have. The difference between this and one shared title across 200k URLs is not marginal._
## The URL count problem
Inventory sites generate URLs faster than they generate value, and submitting all of them is actively harmful. Three filters worth applying before anything reaches your sitemap:
1. **Only index listings worth landing on.** Live, with photographs, with a resolved make and model. A lot with no images and an unparsed title is not a page you want a searcher to arrive at.
2. **Handle the dead ones deliberately.** A sold lot should return `noindex, follow` or redirect to the model category, not sit in the sitemap indefinitely. Never leave a soft 404 returning HTTP 200 with an empty shell.
3. **Set a minimum threshold on category pages.** A category page with two results is thin content. Pick a floor — three, five — and noindex below it. Critically, make the sitemap use the same threshold as the meta robots tag, or you are submitting URLs you are simultaneously telling Google to ignore.
Filter permutations are the other multiplier. `?fuel=diesel&sort=price_low&page=3` should not compete with the clean category URL. Emit `noindex, follow` on filtered variants and canonical them to the base.
> **Fewer, better URLs** — Cutting a sitemap from 600,000 URLs to 250,000 well-formed ones is an improvement, not a loss. Crawl budget spent on empty shells is budget not spent on pages that can rank.
## Structured data that earns its place
JSON-LD is one of the few places where the same work serves both search engines and AI systems, because both read it directly rather than inferring from layout. Worth having:
- `Organization` on the homepage — with a legal name and address if you have them. For anyone asking customers to send money, machine-readable identity is half the trust argument.
- `BreadcrumbList` on every nested page. Cheap, and it renders in results.
- `FAQPage` where you have genuine questions and answers. This is the format AI answers quote from most readily.
- `Article` with `datePublished` and `dateModified` on editorial content. Dates matter more than people expect for freshness signals.
- `Product` or `Vehicle` on listing pages, where the vehicle data supports it honestly.
One rule: structured data must match the visible page. Marking up a price that is not shown, or an FAQ that does not exist on the page, is a manual-action risk and not a clever shortcut.
## Writing for machines as well as people
AI systems reading your pages are doing extraction, not skimming. A few things measurably help, and none of them hurt human readers:
- **Answer the question in the first paragraph.** Extraction favours a direct answer near the heading over one buried after eight hundred words of preamble.
- **Use real heading hierarchy.** One ``, `` for sections, in order. Headings styled with a div and a font size are invisible as structure.
- **Put comparative data in tables.** A genuine ` ` extracts cleanly; a grid of divs does not.
- **State dates and figures explicitly.** "As of August 2026" is usable; "recently" is not.
- **Be specific about uncertainty.** "Terms vary by country — verify with the platform" is more useful to an extraction system than false confidence, and it is more honest to your readers.
### Machine-readable mirrors
An emerging convention worth adopting: publish `/llms.txt` — a plain-text map of your site for language models — and serve Markdown versions of your articles alongside the HTML. It costs almost nothing if your content is structured rather than stored as HTML strings, and it removes any ambiguity about what your pages say.
## Decide the AI crawler question deliberately
Many sites now block AI crawlers, often by accepting a CDN default rather than by making a decision. The distinction that matters is between two different kinds of bot:
| Type | Examples | What blocking costs you |
| --- | --- | --- |
| Search / citation crawlers | OAI-SearchBot, ChatGPT-User, PerplexityBot, Claude-SearchBot | Your site can never be cited in an AI answer. This is a real discovery channel. |
| Training harvesters | CCBot, Bytespider, and similar | Little, in most cases. Blocking these is a defensible default. |
Blocking everything removes the upside while keeping all the downside. Worth noting too that blocking `Google-Extended` does not affect Google Search or AI Overviews, which run on standard Googlebot — so it is a lower-stakes setting than it is often assumed to be.
Whatever you choose, check what is actually being served. CDN-managed rules frequently prepend directives to `robots.txt` and apply firewall rules above it, so the file in your repository may not be the file the crawler receives. Verify with a request carrying the relevant user agent.
## An audit worth running
Ten minutes, and it finds most of what is wrong:
1. `curl` a category page and a listing page. Is there content in the HTML, or an empty div?
2. Do the two pages have different titles and descriptions?
3. Is there a canonical, and does it point where you expect?
4. Do your navigation links appear as `` in the raw HTML?
5. How many URLs are in your sitemap, and what fraction render real content?
6. What does `robots.txt` return over the network, not in your repository?
7. Do sold or expired items still return HTTP 200 with a shell?
8. Are filtered URL variants indexable and competing with clean ones?
If the first question fails, the rest do not matter yet — start with prerendering. [Building a marketplace](/blog/build-used-car-marketplace) covers where this sits in the wider architecture.
### FAQ
**Do AI crawlers execute JavaScript?**
Generally not. The crawlers behind AI answers and citations fetch HTML and read it directly rather than running your bundle. A client-rendered single-page application therefore appears to them as an empty page, not a partially loaded one — so the entire AI citation channel is unavailable regardless of how good the content is.
**Is client-side rendering bad for Google specifically?**
Google does render JavaScript, but in a second pass on a separate queue, while crawl budget is allocated from the first pass. At scale that means large numbers of URLs sharing one title with no text on initial crawl — which resembles a low-quality auto-generated site and wastes budget that could go to pages capable of ranking.
**How do I add SEO to an existing React SPA without rewriting it?**
Add real URLs and real anchor tags first, then prerender the enumerable routes at build time by rendering each with renderToString and injecting the output plus a per-page head into the built HTML template, hydrating on the client. For unbounded listing URLs, inject just the head server-side per request from a single database lookup. Neither step requires a framework migration.
**Should I put every listing URL in my sitemap?**
No. Filter to listings worth landing on — live, photographed, with a resolved make and model — and remove sold or expired items rather than leaving them indefinitely. Make sure the sitemap and your meta robots tags use the same threshold, since submitting URLs you are simultaneously telling Google to ignore is a contradiction that wastes crawl budget.
**Should I block AI crawlers in robots.txt?**
Distinguish the two kinds. Blocking pure training harvesters is a defensible default. Blocking search and citation crawlers means your site can never be the source an AI answer cites, which is a real discovery channel — so blocking everything removes the upside while keeping the downside. Also verify what is actually served, since CDN-managed rules often override the file in your repository.
**What is llms.txt?**
An emerging convention: a plain-text file at the root of a site that maps its content for language models, often paired with Markdown versions of pages served alongside the HTML. It is inexpensive to generate if your content is stored as structured data rather than HTML strings, and it removes ambiguity about what your pages actually say.
---
# Integrating a vehicle auction API: pagination, caching, retries and rate limits
URL: https://thecarapi.com/blog/auction-api-integration-guide
Published: 2026-07-30 · Updated: 2026-08-07
Category: Guides
The first integration always works. The one that survives a traffic spike, a source outage and a schema change is a different piece of software.
This is the article to read after you have made your first successful call and before you put anything in front of users. Everything below comes from integrations that broke in a specific, avoidable way.
## Authentication and key handling
Keys go in a header, never in a query string — query strings end up in access logs, browser history, referrer headers and error trackers.
```bash
# Either form works
curl -H "X-API-Key: $API_KEY" "https://api.thecarapi.com/api/search?limit=1"
curl -H "Authorization: Bearer $API_KEY" "https://api.thecarapi.com/api/search?limit=1"
```
> **Never call the API from the browser** — A key shipped to the client is a public key, whatever your intentions. Proxy through your own backend, which also gives you a place to cache, rate-limit your own users, and swap providers without a frontend release.
One operational detail worth knowing before it bites: repeated authentication failures typically trigger lockouts. A misconfigured staging environment hammering the API with a stale key can lock out the IP that production shares. Keep environments on separate keys and fail fast on a 401 rather than retrying it.
## Cache by data type, not by endpoint
The single highest-leverage decision in the whole integration. The four data layers change at completely different rates, and one TTL across all of them is either far too aggressive or far too conservative.
| Data | Suggested TTL | Reasoning |
| --- | --- | --- |
| Catalog — brands, models, fuels, countries | **24 hours** | Changes when a new nameplate appears. Cache hard. |
| Facet counts | 5 – 15 minutes | Approximate by nature; nobody notices a slightly stale count. |
| Search results | 1 – 5 minutes | Balances freshness against repeated identical queries. |
| Vehicle detail (specification) | 1 hour | Specification does not change. Price does — see below. |
| Current price / bid | **Do not cache** | A cached live price is a wrong price. |
| Image galleries | Days | Effectively immutable once the lot is listed. |
| Price history | 15 minutes | Append-only; staleness only costs you the newest point. |
The detail row deserves care. Specification and price arrive in the same payload but have opposite requirements. The clean resolution is to cache the detail response for the specification fields and re-read price separately on any screen where the user might act on it.
Many GET endpoints are already cached at the edge and return an `X-Cache` header. Log it — an unexpectedly low hit rate usually means you are varying a parameter you did not intend to, such as passing a timestamp or an unstable sort into an otherwise identical query.
## Pagination without falling off the end
Three rules that cover almost every pagination problem in this domain.
### Skip the count when you do not need it
Counting matching rows across a large filtered set is frequently the most expensive part of a search. On infinite scroll or a "load more" pattern you never display the total, so pass `include_total=false` and drop the cost entirely.
### Respect the depth limit
Deep offsets are expensive everywhere, and most APIs cap them. If you need to walk an entire result set — for a nightly reconciliation, say — do not page to offset 50,000. Partition the query instead: iterate by brand, or by year, or by price band, keeping each partition shallow.
```python
# Wrong: falls off the depth limit and gets slower every page
offset = 0
while True:
page = search(brand="bmw", offset=offset, limit=100)
if not page["results"]:
break
offset += 100
# Right: partition so every query stays shallow
for year in range(2015, 2027):
offset = 0
while True:
page = search(brand="bmw", year_from=year, year_to=year,
offset=offset, limit=100, include_total=False)
if not page["results"]:
break
yield from page["results"]
offset += 100
```
### Seed your random sorts
If you sort randomly for variety, the ordering must be stable across pages or page two will repeat rows from page one. The search response returns a `random_seed`; pass it back on subsequent pages.
## Retries, and what not to retry
A retry policy that retries the wrong things turns a small problem into an outage. The rule is: retry only what could plausibly succeed next time.
| Status | Retry? | How |
| --- | --- | --- |
| `429` rate limited | Yes | Honour `Retry-After` if present, otherwise exponential backoff with jitter |
| `500` / `502` / `503` / `504` | Yes | Exponential backoff, cap at 3–4 attempts |
| `408` timeout | Yes | Once, then narrow the query — often a filter is too broad |
| `401` / `403` | **No** | The key is wrong. Retrying triggers lockout protection |
| `400` bad request | **No** | A malformed query will stay malformed |
| `404` | **No** | The lot does not exist, or has been removed |
Use jitter. Without it, every client that failed during a brief outage retries at the same instant and produces a thundering herd on recovery — turning a thirty-second blip into a sustained incident.
```javascript
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
async function call(url, { attempts = 4 } = {}) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(url, { headers: { 'X-API-Key': process.env.API_KEY } });
if (res.ok) return res.json();
if (!RETRYABLE.has(res.status)) {
throw new Error(`${res.status} ${await res.text()}`); // fail fast, do not retry
}
const after = Number(res.headers.get('retry-after')) * 1000;
const backoff = after || 2 ** i * 250 + Math.random() * 250; // jitter
await new Promise(r => setTimeout(r, backoff));
}
throw new Error('exhausted retries');
}
```
## Log the request id
Responses carry a request id, also returned as `X-Request-ID`. Log it with every call, and surface it in your own error messages. It converts a support conversation from "search was slow this morning" into a specific call someone can look up. This costs one line and is the highest-value logging you will add.
Log `contract_version` too. When a response shape changes, the version in your logs tells you exactly when, which is otherwise a genuinely difficult thing to reconstruct.
## Handle sparse fields properly
Fields are present when the source provides them, which means absent fields are the normal case rather than an error. Two habits:
- **Never assume a field exists.** VIN, horsepower, photo count and equipment lists are all commonly absent. Destructuring without a default is the most common cause of a crash in a vehicle frontend.
- **Distinguish null from zero.** A mileage of `0` means the odometer read zero. `null` means the source did not say. Rendering both as "0 km" makes every unknown-mileage car look new.
> **Monitor fill rates, not just errors** — A field that was 95% populated and drops to 40% is a broken upstream extractor, and it will not raise a single error in your logs. Track fill rate per field per source over time — it catches an entire class of silent data degradation that error monitoring is blind to.
## Health checks and graceful degradation
A vehicle marketplace does not need to go down because one endpoint is slow. Decide in advance what degrades and how:
1. If **images** fail, render cards with a placeholder. Do not fail the page.
2. If **facets** fail, render search without filter counts. Users can still search.
3. If **price history** fails, hide the chart. It is an enhancement, not the product.
4. If **search** fails, that is a real outage — surface it honestly rather than showing an empty result set, which users read as "no cars match" and act on.
That last distinction matters more than it looks. An empty result set and a failed request are completely different messages to a user, and conflating them makes your product look like it has no inventory.
```bash
curl -s "https://api.thecarapi.com/api/health"
```
_Cheap to poll, and worth wiring into your own status page rather than discovering an issue from users._
## A pre-launch checklist
- API key is server-side only, and staging uses a different key from production.
- Cache TTLs are set per data type; live prices are not cached.
- Retries cover only retryable statuses, with jitter and an attempt cap.
- `include_total=false` on any view that does not display a count.
- Deep pagination is partitioned rather than offset-walked.
- Request ids are logged and surfaced in errors.
- Every field access tolerates absence; null and zero are distinguished.
- Degradation behaviour is defined per endpoint, and empty results are visibly distinct from failures.
- Fill-rate monitoring is running before launch, not after the first data incident.
Endpoint-level detail for everything referenced here is in [the API reference](/docs), and [building a marketplace](/blog/build-used-car-marketplace) covers the architecture these mechanics sit inside.
### FAQ
**How long should I cache vehicle auction API responses?**
By data type rather than by endpoint. Catalog values such as brands and fuels can cache for 24 hours; facet counts 5–15 minutes; search results 1–5 minutes; vehicle specification around an hour; image galleries for days. Current prices and bids should not be cached at all — a cached live price is simply a wrong price.
**Which HTTP errors should I retry?**
Retry 408, 429 and 5xx with exponential backoff and jitter, capped at three or four attempts, honouring Retry-After when present. Never retry 400, 401, 403 or 404 — those will not succeed on a second attempt, and repeatedly retrying authentication failures can trigger lockout protection that affects your whole environment.
**How do I export a large result set without hitting pagination limits?**
Partition the query instead of walking deep offsets. Iterate over brands, years or price bands so each individual query stays shallow, and pass include_total=false since you are not displaying a count. Deep offsets get progressively more expensive and are usually capped, so an offset-walk over tens of thousands of rows will fail or time out.
**Can I call the auction API directly from my frontend?**
No. Any key shipped to a browser is public regardless of intent. Proxy through your own backend, which also gives you a natural place to cache responses, apply your own per-user rate limiting, and change providers without shipping a frontend release.
**Why do fields sometimes disappear from responses?**
Fields are present when the source publishes them, so absence is normal rather than exceptional — VIN, horsepower and equipment lists are frequently missing. A sudden drop in how often a field appears, though, usually indicates a broken upstream extractor. That produces no errors, so it is only caught by monitoring fill rate per field per source over time.
---
# Building a used car marketplace without owning inventory
URL: https://thecarapi.com/blog/build-used-car-marketplace
Published: 2026-07-21 · Updated: 2026-08-07
Category: Guides
The hard part of a marketplace is never the listings page. It is having something to list on day one, and having a reason for anyone to use you rather than the source.
Every marketplace starts empty, and an empty marketplace has no reason for anyone to visit, which means no sellers, which means it stays empty. In most categories you break that loop with sales effort. In vehicles you can sidestep it, because wholesale inventory already exists in volume and is reachable programmatically.
That solves the cold start and immediately creates the real question: if your listings come from the same feed anyone can license, what makes your product worth using? This walks through both — the architecture, and the part that has to be yours.
## Decide what you actually are
Four viable shapes, with materially different technical requirements. Choosing late is expensive.
| Model | You provide | Revenue | Hardest part |
| --- | --- | --- | --- |
| Discovery portal | Search and comparison, referral out | Referral or subscription | Traffic acquisition |
| Brokerage | Search plus you transact on the buyer's behalf | Commission per deal | Operations, trust, logistics |
| Dealer tooling | Sourcing and pricing for trade buyers | SaaS subscription | Beating a spreadsheet |
| Regional importer | Curated foreign stock, landed prices in local currency | Margin per vehicle | Import compliance |
The discovery portal is the easiest to build and the hardest to sustain, because you are competing on SEO against the sources themselves. The regional importer is the opposite: real operational work, but a genuine reason to exist, because the buyer cannot access German or Korean wholesale supply directly and does not want to handle the import.
## The data layer
The decision that shapes everything downstream: proxy the API on every request, or maintain your own copy?
### Proxy-through
Your backend forwards search requests to the provider and returns the response. Nothing is stored. It is fast to build and always fresh, and it is the right starting point.
It stops working when you need to join provider data against your own — favourites, sold-by-you flags, custom pricing, per-vehicle notes — or when your traffic makes per-request calls expensive, or when you need search behaviour the provider does not offer.
### Local mirror
You sync inventory into your own database and search it locally. More moving parts, and it introduces a staleness window you now own — but it lets you rank, join and enrich freely.
> **The pragmatic middle** — Mirror the search-card fields you need for listing and filtering. Fetch detail, images and price history live on demand. You get local ranking and joins without duplicating the heavy, fast-changing parts — and your staleness exposure is limited to a small, well-understood field set.
Whichever you choose, key everything on the source pair. Your favourites table stores `openlane/11125938`, not a synthetic id that will not survive a resync.
```sql
create table saved_vehicle (
id bigserial primary key,
user_id bigint not null references app_user(id),
site_name text not null, -- openlane | auto1 | encar | ...
auction_id text not null, -- the platform's own lot id
saved_at timestamptz not null default now(),
-- denormalized so a saved list still renders after the lot closes
title_snapshot text,
price_eur_snapshot numeric(12,2),
unique (user_id, site_name, auction_id)
);
```
_Snapshotting title and price is not redundancy — it is what stops a user's saved list turning into a page of dead references once lots close._
## Search is your product
Users judge a vehicle marketplace almost entirely on search. Four things carry most of the perceived quality:
1. **Facet counts that are live.** A filter option showing zero results, or showing results that do not exist, destroys trust faster than any other defect. Drive facets from the API rather than a static list.
2. **Sensible defaults.** Newest-first or best-value-first beats a raw price sort, which surfaces the least desirable stock at the top of every page.
3. **Fast pagination.** Counting total matches is often the most expensive part of a search. On infinite scroll, request `include_total=false` and skip it.
4. **Stable ordering under randomisation.** If you shuffle results for variety, use a seed so page two does not repeat page one.
```bash
# Page 1 — note the returned random_seed
curl -s "https://api.thecarapi.com/api/search?sort=random&limit=24&include_total=false" \
-H "X-API-Key: $API_KEY"
# Page 2 — pass the seed back so the ordering stays consistent
curl -s "https://api.thecarapi.com/api/search?sort=random&seed=k9f2ab\
&limit=24&offset=24&include_total=false" \
-H "X-API-Key: $API_KEY"
```
Build the filter UI from [the catalog endpoints](/docs/catalog) rather than hardcoding brand and fuel lists. Source vocabularies drift, and a hardcoded value that changes upstream fails silently as an empty result set — the worst kind of bug, because it looks like a market condition.
## Images decide how the product feels
Rules that matter more here than in most product categories, because the listing grid is the product:
- Thumbnail only on search cards. Never fetch galleries for a results page.
- Set explicit width and height so the grid does not reflow as images arrive.
- `loading="lazy"` below the fold; eager for the first row.
- Generate alt text from the vehicle data you already have — it is free accessibility and free image SEO.
- Never hotlink source URLs. [They expire and are hotlink-protected](/blog/vehicle-image-cdn-api), and the failure is delayed enough to reach production.
## The part that has to be yours
If your entire product is a nicer frontend over a feed, your competitive position is a design refresh away from disappearing. The defensible layer is always something the feed cannot contain:
- **Landed cost in the buyer's currency and jurisdiction.** Transport, duty, VAT, registration, your fee — one number a buyer can act on. Genuinely hard, genuinely valuable, and specific to a market you understand.
- **Curation.** Rejecting 95% of available stock is a service. "Every car in Europe" is not a value proposition; "the forty cars worth your attention this week" is.
- **Trust infrastructure.** Escrow, inspection, delivery, a real company with a real address. This is the entire product for cross-border buyers.
- **A proprietary signal.** Your own repair-cost model, your own local demand data, your own dealer network's bid history.
- **Workflow.** For trade buyers, integration into how they actually work — CRM, stock management, alerts on saved searches — beats a better search box.
> Nobody wins a market by showing the same listings as everyone else, slightly later. The feed is the floor you build on, not the building.
## A launch sequence
1. **Week 1** — proxy search and detail straight through. No database, no auth. Prove the funnel: do people search, and do they click?
2. **Week 2–3** — accounts, saved vehicles, saved searches. This is the first thing that makes returning worthwhile.
3. **Week 4–6** — the mirror, if search performance or joins demand it. Not before.
4. **Week 6+** — the defensible layer. Landed cost, curation, or whichever of the above fits your market.
5. **Continuously** — SEO. Vehicle marketplaces live on organic search, and that means server-rendered listing pages with real URLs, not a client-only app. [See how we handle it](/blog/seo-for-data-driven-sites).
> **Do not build the mirror first** — The most common failure is spending six weeks on sync infrastructure before finding out whether anyone wants the product. Proxy-through gets you a testable marketplace in days. Add the database when a specific limitation forces it — and it will tell you clearly when it does.
For the integration mechanics — pagination, caching, retries, rate limits — [the integration guide](/blog/auction-api-integration-guide) covers the details, and [the API reference](/docs) documents every endpoint used here.
### FAQ
**Can I build a car marketplace without owning any vehicles?**
Yes — it is the standard way to solve the cold-start problem in this category, because wholesale inventory already exists in volume and is reachable programmatically. The consequence is that inventory stops being a differentiator, so the product needs something the feed cannot supply: landed cost, curation, trust infrastructure, or workflow integration.
**Should I mirror the inventory into my own database or proxy the API?**
Start by proxying — it is faster to build, always fresh, and lets you validate the product in days. Move to a mirror only when a specific need forces it: joining against your own data, ranking the provider does not offer, or per-request cost at your traffic level. A common middle ground is mirroring search-card fields while fetching detail and images live.
**What should I use as the primary key for vehicles?**
The source platform slug together with the platform's own lot id — for example `openlane/11125938`. It is stable, survives resyncs, and is what every detail, image and price-history endpoint is addressed by. Also snapshot the title and price in tables like saved vehicles, so a user's list still renders after the underlying lot closes.
**How do I compete with the auction platforms themselves?**
Not on inventory, since it is the same stock. Compete on something the source cannot offer: landed cost in the buyer's currency and jurisdiction, curation that rejects most of the catalogue, trust infrastructure for cross-border purchases, or workflow integration for trade buyers. Access to supply the buyer cannot reach directly is itself the value in the importer model.
**What is the most common mistake when launching one of these?**
Building sync infrastructure before validating demand. Teams spend six weeks on a mirror, ranking and caching before learning whether anyone wants the product. Proxying the API directly gets a testable marketplace live in days, and the limitations that justify a database announce themselves clearly when they arrive.
---
# Used car market value: wholesale prices, retail asking prices, and the spread
URL: https://thecarapi.com/blog/used-car-market-value-comparables
Published: 2026-07-08 · Updated: 2026-08-07
Category: Market intelligence
The gap between what the trade pays and what the market asks is where every margin in this industry lives. Measuring it correctly requires two datasets that must not be merged.
"What is this car worth?" is not one question. It is at least four, with genuinely different answers, and a valuation product that does not say which one it is answering will produce numbers that are individually defensible and collectively incoherent.
| Question | Answer comes from | Typical relationship |
| --- | --- | --- |
| What will the trade pay at auction? | Auction clearing prices | The floor |
| What is it listed for retail? | Classifieds asking prices | The ceiling — an aspiration |
| What does it actually retail for? | Asking price minus negotiation | Below the ceiling, by a variable amount |
| What will it cost me landed? | Hammer + fees + transport + duty + VAT | Above the floor, sometimes far above |
Confusing the first and second is the most common error, and it is expensive in both directions: it makes auction stock look like a bargain to a buyer, and makes a dealer's margin look larger than it is.
## Why asking prices are not transaction prices
Classifieds data is abundant and easy to collect, which is exactly why it is over-trusted. An asking price is a seller's opening position, and four biases sit between it and reality:
- **Negotiation.** Most private and many dealer sales close below the advertised figure. The discount varies by market, by model and by how motivated the seller is.
- **Survivorship, running backwards.** The ads you can see today are disproportionately the ones that have not sold. Well-priced cars leave the dataset quickly; overpriced ones accumulate in it. A naive average of live ads is therefore biased upward.
- **Listing age.** A three-day-old ad and a ninety-day-old ad at the same price are telling you completely different things about that price.
- **Presentation premium.** Retail prices include reconditioning, warranty, preparation and a physical location. None of that is in a wholesale figure, so the spread is not pure margin.
> **The survivorship bias is the one that gets missed** — It is counter-intuitive because it runs the opposite way to the familiar version. Fast-selling cars disappear from the visible set, so the live-ads average drifts upward over time even in a flat market. Weighting by listing age is a partial correction; treating asking prices as transaction prices is not.
## Keeping the two datasets apart
It is tempting to merge auction lots and classifieds into one searchable index. It produces a search result nobody can act on: a user filtering for cars under €10,000 gets a mixture of things they can bid on and things they can buy, at prices computed on incompatible bases, with no indication of which is which.
We keep them structurally separate for that reason — auction sources are one surface, [European retail classifieds](/docs/theparking) are another, and they are never merged into the same search. The classifieds data exists to answer "what does this retail for", not to pad the inventory count.
```bash
# Wholesale: what the trade is paying
curl -s "https://api.thecarapi.com/api/search?brand=bmw&model=320d\
&year_from=2019&year_to=2021&limit=50" \
-H "X-API-Key: $API_KEY"
# Retail: what the same car is asked for, across European portals
curl -s "https://api.thecarapi.com/api/theparking/listings?brand=BMW\
&country=de,at,nl&price_to=30000" \
-H "X-API-Key: $API_KEY"
```
_Two endpoints, two datasets, two meanings. The comparison is yours to make deliberately._
## Constructing a defensible reference price
A method that survives being questioned by someone with money at stake:
1. **Define the comparable set narrowly.** Same make, model, registration year, fuel, gearbox and damage state. Mileage within a proportional band — ±15% rather than a fixed kilometre figure.
2. **Prefer clearing prices to live observations.** A closed sale is a fact. A live asking price is a proposal.
3. **Adjust for mileage explicitly** rather than pooling. State the adjustment so it can be checked.
4. **Bound the recency window** to 60–90 days. Older data averages across a market that has moved.
5. **Report the sample size.** An estimate from four observations and one from four hundred must not be presented identically.
6. **Report the dispersion, not just the centre.** A tight cluster and a bimodal spread with the same median mean very different things about confidence.
The last two are what separate a reference price from a guess with a decimal point. A user who can see that the estimate rests on six cars with a wide spread will treat it appropriately; one shown a bare number will not.
```json
{
"auction_id": 11125938,
"site_name": "openlane",
"public_price_eur": 9800,
"market_reference": {
"price_eur": 12400,
"mileage": 71000,
"km_difference": -6205,
"explanation": "Reference from comparable listings, adjusted for mileage difference"
}
}
```
_Returned by `/api/top-offers`. The reference travels with the lot, so a user who disagrees with the signal can see what it was based on._
## Reading the spread
Once you have both numbers for the same vehicle, the ratio between them is more informative than either alone — but it needs interpreting, because a wide spread has several possible causes:
- **Genuine margin.** Real, and the basis of the trade.
- **Reconditioning cost.** The retail car has had work the auction car has not. Not margin.
- **Time cost.** Weeks of holding, financing and forecourt space between the two prices.
- **Condition difference the data does not capture.** The most likely explanation for an outlier, and the most dangerous to ignore.
- **Market segment mismatch.** A wholesale lot compared against a retail listing for a slightly different trim.
A practical heuristic: an apparent spread far above the norm for that model is more often a data problem than an opportunity. Investigate the comparable set before acting on it.
## Regional variation is signal, not noise
The same car does not cost the same across Europe, and the differences are systematic rather than random — driven by local taxation, emissions rules, fuel-type preference, climate and fleet composition. Diesel demand varies sharply between markets. Some countries apply registration taxes that make certain vehicles structurally more expensive.
For a valuation product this means a single European reference price is usually the wrong abstraction. Compute per-market references where you have the sample size, and be explicit about which market a number refers to. For a sourcing product, the variation is the whole opportunity.
> **Cross-border comparison needs landed cost** — A car cheaper in one country is only cheaper after transport, registration and — for non-EU sources — duty and import VAT. Comparing raw prices across borders without landed cost produces arbitrage that evaporates on contact with reality. See [the calculator endpoints](/docs/calculator).
## What to build first
If you are adding valuation to an existing product, the order that delivers value soonest:
1. A comparable set with the sample size shown. Even without a modelled price, showing a user the ten most similar recent sales is immediately useful and hard to argue with.
2. A mileage-adjusted reference price on top of that set.
3. The retail spread, once you have retail data for the same models.
4. Per-market breakdowns, once your sample supports splitting.
5. Trend over time, which requires the archive — and which is the reason [price history](/blog/car-price-history-api) needs to be collected before you need it.
### FAQ
**What is the difference between wholesale and retail used car prices?**
Wholesale is what the trade pays at auction — a transaction price between professionals. Retail asking price is what a seller advertises to a consumer, and it includes reconditioning, warranty, preparation and premises, as well as room to negotiate. They measure different things and should never be pooled into a single average.
**Why are classifieds asking prices biased upward?**
Survivorship, running in the unfamiliar direction. Well-priced cars sell quickly and leave the visible set, while overpriced ones remain and accumulate. An average of currently-live advertisements therefore drifts above the true transaction level even in a flat market. Weighting by listing age partially corrects for it.
**How many comparable vehicles do you need for a reliable price estimate?**
There is no single threshold, which is why the sample size should be shown rather than hidden. An estimate from four observations and one from four hundred are both legitimate outputs but carry completely different confidence, and presenting them identically misleads the user. Report dispersion alongside the central figure.
**Should auction listings and classifieds be searchable together?**
No. They have different price bases, different availability semantics — one you bid on, one you buy — and different data shapes. Merging them produces search results a user cannot act on. Keep them as separate surfaces and use the classifieds data to answer "what does this retail for" rather than to inflate an inventory count.
**Does the same car have the same value across Europe?**
No, and the variation is systematic rather than random — driven by local taxation, emissions regulation, fuel-type preference and fleet composition. A single European reference price is usually the wrong abstraction; compute per-market references where the sample supports it, and always include landed cost before treating a cross-border price difference as an opportunity.
---
# Vehicle images at scale: why hotlinking auction galleries fails
URL: https://thecarapi.com/blog/vehicle-image-cdn-api
Published: 2026-06-22 · Updated: 2026-08-07
Category: Data engineering
Images are the largest, most expensive and most fragile part of vehicle data — and the part teams discover late, usually in the form of a listings grid full of broken thumbnails.
A wholesale vehicle listing carries somewhere between twenty and sixty photographs. At a million live vehicles that is tens of millions of images — and unlike the specification fields, users actually look at these. A listing grid where a third of the thumbnails are broken reads as a broken product, regardless of how good the underlying data is.
The naive approach is to store the source image URL alongside the listing and let the browser fetch it. It works in development. Here is why it does not survive.
## Four reasons hotlinking fails
### 1. Source URLs expire
This is the fatal one. Auction platforms commonly stop serving media for lots that have closed — sometimes within days, sometimes after weeks, and rarely with any announcement. Live listings look fine. Your historical archive rots quietly from the oldest end, and by the time anyone notices, months of galleries are gone with no way to recover them.
### 2. Hotlink protection
Many sources check the `Referer` header or require a valid session cookie. The failure mode is genuinely nasty: images load for you (you visited the source recently), fail for your users, and behave differently across browsers depending on referrer policy. It presents as an intermittent bug and is frequently misdiagnosed for weeks.
### 3. File sizes built for a different use case
Source galleries are full-resolution JPEGs, commonly 2–6 MB each. Loading a twenty-four-card grid with one thumbnail each means fetching perhaps 60 MB to render a page that needs about 400 KB of imagery. On mobile that is not slow, it is unusable.
### 4. You have no control over availability
Your product's perceived performance becomes a function of seven third-party CDNs you have no relationship with, no SLA against, and no cache control over. One source having a slow morning becomes your incident.
> **The failure is silent and delayed** — Every one of these problems is invisible during development and for the first few weeks in production. They surface together, later, as "the site looks broken" — which is why image strategy belongs in the initial architecture rather than in a follow-up ticket.
## What an image vault has to do
Copying images is the easy part. These are the properties that determine whether the copy is actually worth its storage cost:
1. **Copy at ingest, not on demand.** Lazy copying means the first user to view an old lot triggers a fetch against a URL that may already be dead. Copy when the listing is first seen, while the source still serves it.
2. **Re-encode.** WebP at sensible quality typically cuts payload substantially against source JPEG with no visible difference in a listing grid.
3. **Serve multiple sizes.** A thumbnail grid and a full-screen gallery have completely different requirements, and serving one image for both wastes bandwidth in one direction or detail in the other.
4. **Stable URLs that outlive the lot.** The entire point. If the served URL breaks when the auction closes, you have built an expensive cache rather than an archive.
5. **Preserve ordering and role.** Which image is primary, which section of the vehicle each belongs to, exterior versus interior versus damage detail. Galleries presented in arbitrary order are noticeably worse to use.
6. **Record dimensions.** Without width and height in the payload, every image causes layout shift on load, which is both a user-experience problem and a Core Web Vitals penalty.
```bash
curl -s "https://api.thecarapi.com/api/auction-images/encar/38112900" \
-H "X-API-Key: $API_KEY"
```
_Galleries are addressed by the same `site/auction_id` pair as detail and price history, and return `served_url` alongside ordering, section and dimensions._
## Consuming galleries efficiently
Two patterns that account for most of the practical difference in page performance.
### Do not fetch galleries in search
Search results need one thumbnail per card. Full gallery payloads for twenty-four results is a large response containing data for images the user will almost certainly never open. Use the thumbnail on the search card and fetch the gallery only when a detail view opens.
```javascript
// Search card — thumbnail only, from the search response
// Detail view — gallery fetched on open, first image eager
const images = await fetch(
`${API}/api/auction-images/${site}/${id}`,
{ headers: { 'X-API-Key': key } }
).then(r => r.json());
```
### Write real alt text
Vehicle imagery has an unusual advantage here: you have structured data describing exactly what is in the picture. `alt="2021 Kia Niro, exterior front three-quarter"` costs nothing to generate, is genuinely useful to screen reader users, and is one of the few places where accessibility and image SEO point in exactly the same direction. Empty alt attributes on a page whose entire content is vehicles is a wasted signal.
> **Dimensions prevent layout shift** — If the API returns width and height, set them on the element. Reserved space means the grid does not jump as images arrive — the single cheapest improvement available to an image-heavy listing page, and it is measured directly by Cumulative Layout Shift.
## The storage maths
Worth doing explicitly, because the intuition is usually wrong in both directions.
| | Source JPEG | WebP, re-encoded |
| --- | --- | --- |
| Average image | ~2.5 MB | ~150 KB |
| One listing (30 images) | ~75 MB | ~4.5 MB |
| 1M live listings | **~75 TB** | **~4.5 TB** |
_Indicative figures — actual ratios depend on source quality and target encoding settings._
Storage at a few terabytes is inexpensive. The costs that actually hurt are elsewhere: the residential-proxy bandwidth to fetch the originals in the first place, and the CDN egress to serve them afterwards — which scales with your traffic rather than your catalogue, and is therefore the bill that grows exactly when things are going well. [The scraping cost breakdown](/blog/scraping-car-auction-sites-vs-api) puts both lines in context.
## Rights, briefly
Listing photographs are copyrighted works, and the position on re-hosting them is genuinely distinct from the position on extracting factual specifications. This is not legal advice and the answer varies by jurisdiction and by the terms you are operating under — but it is a real consideration, and it is one of the substantive differences between collecting images yourself and consuming them from a provider who has a commercial relationship with the sources.
For the fuller picture on what auction data contains and how to consume it, start with [the auction data guide](/blog/car-auction-api-guide), or see [the image endpoints in the reference](/docs/auctions).
### FAQ
**Why do auction listing images break after a few weeks?**
Source platforms commonly stop serving media for lots that have closed, often within days or weeks of the auction ending. Live listings continue to work, so the problem is invisible at first and shows up as historical galleries decaying from the oldest end — with no way to recover images you did not copy while the source was still serving them.
**Can I just link directly to source auction images?**
Only if you never need to show a listing after it closes. Direct linking also runs into hotlink protection that checks the request referrer — which causes intermittent failures that look like a browser bug — and serves multi-megabyte originals to mobile users. For anything with a historical view, images have to be copied at ingest.
**How much storage do vehicle images actually need?**
Roughly 4–5 TB for a million listings at thirty images each once re-encoded to WebP, against roughly 75 TB for the source JPEGs. Storage itself is cheap; the expensive lines are the bandwidth to fetch the originals over residential proxies and the CDN egress to serve them, the latter scaling with your traffic rather than your catalogue size.
**What should vehicle image alt text say?**
Describe the vehicle and the view: "2021 Kia Niro, exterior front three-quarter". You already hold structured data for make, model and year, so this can be generated automatically. It is one of the few cases where the accessibility requirement and the image SEO opportunity are satisfied by exactly the same text.
**Should search results include full image galleries?**
No. Search cards need one thumbnail each; returning thirty image records per result multiplies the response size for data the user will almost certainly never open. Use the thumbnail in the search payload and fetch the gallery on demand when a detail view opens.
---
# Salvage and damaged vehicle data: fields, sources and pitfalls
URL: https://thecarapi.com/blog/salvage-damaged-vehicle-data
Published: 2026-06-03 · Updated: 2026-08-07
Category: Data engineering
Damaged vehicles are not cheap versions of clean ones. They are a separate market, and treating them as a filter on the main one produces a product nobody in that market can use.
Salvage buyers are repairers, dismantlers and parts operations. They are not looking for a car; they are looking for a specific repair economics calculation to come out positive. That makes almost every assumption baked into a normal vehicle search wrong for them — including the one where price ascending is a useful sort.
This covers what damage data actually looks like across sources, why unifying it into one rich schema fails, and the modelling decisions that make the difference between a usable product and a filter.
## There is no European damage standard
This is the fact that shapes everything else. Unlike the US, where salvage title branding is a regulated, reasonably consistent concept, European damaged-vehicle disclosure varies by country, by platform and by insurer. What you get differs enormously by source:
| Source type | Typical damage disclosure | Usable for |
| --- | --- | --- |
| Salvage specialist (e.g. Schadeautos) | Detailed free text or structured per-area description, often with cause | Repair estimation, parts sourcing |
| Salvage auction (e.g. Copart Germany) | Damage category plus extensive photography, runs/drives indicators | Bidding decisions, triage |
| General wholesale | A boolean, sometimes a short note | Excluding damaged stock from clean searches — little more |
| Remarketing / fleet | Wear-and-tear grading against a contractual standard | Reconditioning cost estimation, not accident damage |
That last row is a distinction worth being pedantic about. Fleet wear grading and accident damage are different concepts that both end up in fields called something like "condition". A car graded down for kerbed alloys and seat wear is not damaged in the sense a salvage buyer means.
## Why unifying damage into one schema fails
The instinct is to design a rich damage schema — affected panels, severity per area, structural yes/no, airbag deployment, water ingress — and map every source onto it. It is the right schema. It is also, in practice, 80% null, because most sources do not publish at that granularity and nothing can conjure the data.
Worse, the nulls are not random. They correlate with source, which means any analysis over the schema is really an analysis of which sources publish detail. That is a subtle enough failure to survive review and reach production.
> **Never synthesise severity from free text** — It is tempting to classify "front end damage, airbags deployed" as structural with a keyword rule. It works on the examples you tested and fails on the edge cases — which are disproportionately the expensive vehicles, where being wrong costs the most. If a source did not state severity, the honest value is unknown.
## The layered model that works
Three tiers, each honest about its own coverage:
1. **A reliable boolean.** `has_technical_damage` or equivalent, populated for every vehicle from every source. Coarse, but it is what the overwhelming majority of users are actually filtering on, and it must never be null.
2. **A severity band where the source supports it** — light / structural / total loss — populated only where stated, explicitly unknown otherwise.
3. **The raw damage payload**, unmodified, for sources that publish detail. Repair estimators read this and ignore the tiers above it entirely.
```bash
# Damaged stock only, across every source that has it
curl -s "https://api.thecarapi.com/api/search?damaged=true&brand=vw\
&year_from=2019&sort=price_low&limit=24" \
-H "X-API-Key: $API_KEY"
# Full detail on one lot — the raw source damage payload lives here
curl -s "https://api.thecarapi.com/api/auction/schadeautos/1775437" \
-H "X-API-Key: $API_KEY" | jq '.car_identification'
```
## Photographs are the actual data
For damaged stock this is not a nicety. No text description substitutes for forty photographs of the damage, and buyers make decisions from images in a way they do not for clean cars. Three consequences:
- **Gallery completeness is a quality metric.** A damaged listing with four photos is close to worthless regardless of how good its text is. Consider exposing photo count so users can filter on it.
- **Image durability matters more here.** Post-sale analysis of damaged stock — what did this repair actually cost against what the photos showed — is a core workflow, and it breaks entirely if source URLs expire. [Why an image vault beats hotlinking](/blog/vehicle-image-cdn-api) covers the mechanics.
- **Resolution matters.** Aggressive downscaling that is fine for a clean-car thumbnail grid destroys the detail a repairer is looking for. Keep a large variant.
## Copart Germany, and why the customs point matters
Copart operates salvage auctions internationally, and a lot of English-language material about buying Copart stock is written from a US perspective. For a European buyer that material is actively misleading, because it describes a cross-border import.
German Copart stock is inside the EU customs union. For an EU buyer that means no third-country import duty and no import VAT event — a materially different landed cost from US salvage on the same hammer price. If your product quotes a landed figure, this has to be driven by the source and origin country rather than by one formula.
> **A common and expensive mix-up** — Applying US-salvage import logic to German salvage overstates the cost; applying EU-internal logic to genuinely third-country stock understates it. Both errors reach the customer as a wrong number. See [the calculator endpoints](/docs/calculator) for the per-source fee model.
## What the damaged-stock buyer needs from a search
Design decisions that follow from the buyer being a repairer rather than a driver:
- **Model and year matter more than trim.** Parts compatibility is the constraint, and it is determined by platform and generation.
- **Mileage matters less than usual.** A 180,000 km car with a repairable front end can be a better buy than a 40,000 km one with structural damage.
- **Cheapest first is often the wrong default sort.** The cheapest damaged car is usually the one nobody can economically repair. Sorting by price relative to a clean reference is far more useful.
- **Batch discovery is normal.** Repairers want every repairable Golf VII in a region right now, not one perfect result. Pagination depth and result-set completeness matter more than ranking quality.
- **Runs-and-drives status, where published, is close to a primary filter.** It changes the economics more than most specification fields.
## Modelling recovery value
The question underneath every salvage purchase is: what is this worth once repaired, minus what the repair costs? The first half is tractable from data you can have — a clean-condition reference for the same model, year and mileage band, which is exactly what [comparables](/blog/used-car-market-value-comparables) produce.
The second half is not, and it is worth being clear about that rather than pretending otherwise. Repair cost depends on parts availability, labour rates, hidden damage that photographs do not show, and the buyer's own capabilities. The useful thing a data product can do is supply the clean reference accurately and let the buyer bring their own repair estimate — not to produce a confident number for a variable it cannot observe.
For the wider normalization context around damage fields, see [vehicle data normalization](/blog/vehicle-data-normalization).
### FAQ
**Is there a standard damage classification for European vehicles?**
No. Unlike US salvage title branding, European damaged-vehicle disclosure varies by country, platform and insurer. Salvage specialists publish detailed descriptions, salvage auctions publish categories plus heavy photography, and general wholesale platforms often publish only a boolean. Any unified rich schema will be mostly null, and the nulls will correlate with source.
**How should damaged vehicle data be modelled?**
In three layers. A reliable boolean populated for every vehicle from every source, which is what most users filter on. A severity band populated only where the source states it, explicitly unknown otherwise. And the raw source damage payload preserved unmodified, which is what repair estimators actually read.
**Can severity be inferred from a free-text damage description?**
It should not be. Keyword rules work on the examples you test and fail on edge cases, which are disproportionately the expensive vehicles where being wrong costs most. If the source did not state severity, the honest value is unknown — a null a user can see is safer than a guess they cannot.
**Does buying Copart Germany salvage incur EU import duty?**
German Copart stock sits inside the EU customs union, so for an EU buyer there is no third-country import duty and no import VAT event — unlike US-sourced salvage. Much English-language guidance about Copart is written from a US perspective and does not apply. Landed-cost logic has to branch on the origin country rather than applying one formula.
**Why is price-ascending a bad default sort for damaged stock?**
Because the cheapest damaged vehicle is usually the one nobody can economically repair — severe structural damage or a total loss with no viable path back. Salvage buyers are solving for repair margin, so sorting by price relative to a clean-condition reference for the same model surfaces genuinely interesting lots that a raw price sort buries.
---
# Car price history: reading auction price movement over time
URL: https://thecarapi.com/blog/car-price-history-api
Published: 2026-05-19 · Updated: 2026-08-07
Category: Market intelligence
A price is a fact about a moment. A price series is a fact about a market — and it is the only one you cannot go back and collect later.
Nearly every question worth asking about a vehicle listing is comparative. Is this cheap? Is the seller moving on price? Has this model softened this quarter? None of those can be answered from the listing in front of you. They all need history, and history has an awkward property: it only accrues in real time. You cannot buy back the six months you did not record.
This is about what auction price series actually look like, the interpretation errors that make them worse than useless, and what you can build once you have them.
## The shape of an auction price series
A wholesale lot generates several distinct kinds of price observation, and conflating them is the first and most damaging mistake.
| Observation | What it means | Safe to compare across lots? |
| --- | --- | --- |
| Opening / start price | Where the seller chose to begin | Weakly — it reflects strategy, not value |
| Current bid | Highest bid so far, possibly below reserve | **No** — it is a floor, not a value |
| Buy-now price | A price at which the car is actually available | Yes |
| Reserve met / not met | Whether the current bid is transactable | Essential context for any of the above |
| Final / hammer | What it actually cleared at | Yes — this is the real signal |
| Relisted price | A second attempt after failing to sell | Yes, but flag it as a relist |
The current-bid row is where most naive models go wrong. A lot sitting at €4,200 with an unmet reserve of €9,000 is not a €4,200 car. Feed unmet-reserve current bids into a valuation model as if they were sale prices and the model will systematically underestimate the market — badly, and in a way that looks fine on aggregate metrics.
> **The relist trap** — A car that fails to sell and is relisted appears as two lots. Count both as observations of market activity and you double-count the same vehicle, over-weighting exactly the cars the market did not want. Deduplicate on VIN where available, and on a specification-plus-mileage signature where it is not.
## What a price change actually tells you
When an observed price moves, there are at least five possible causes, only one of which is "the market changed":
1. A genuine bid arrived — real demand signal.
2. The seller reduced the asking or buy-now price — a supply-side signal about their urgency, which is different information.
3. The auction moved to a new phase with different mechanics.
4. An FX rate moved, on a non-euro source. Nothing about the car changed.
5. A fee or VAT basis changed upstream, so the *same* underlying price is now presented differently.
Causes four and five are why a price series needs to record more than a number and a timestamp. If you cannot tell an FX move from a price cut, your "market softening" chart is partly a currency chart.
```bash
curl -s "https://api.thecarapi.com/api/auction/openlane/11125938/price-history" \
-H "X-API-Key: $API_KEY"
```
_The series carries both the source-currency figure and the comparable EUR figure at each observation, along with which fields changed — so a currency move is distinguishable from a price move._
## Sampling, and the gaps in your series
Any price history is a sampled series, not a continuous record. Two properties follow, and both matter for interpretation.
First, **you see changes, not moments.** If the observation interval is a few minutes, a burst of bidding in the final ninety seconds of an auction may be compressed into one or two points. The series will show the endpoints correctly and understate the volatility in between.
Second, **absence of a point is not absence of change.** A gap in the series means nothing was observed to have changed, which is different from nothing having changed — particularly around the close of an auction, when the interesting activity is densest and the pipeline is under the most load.
> **Prefer final over trajectory** — For most analytical purposes the clearing price is far more valuable than the path taken to it. Bid trajectory is interesting for auction-dynamics research; if you are building valuation or comparables, weight the final observation heavily and treat the path as context.
## Building comparables
The practical output most teams want is: given this car, what have similar cars actually sold for recently? Building that is mostly a question of defining "similar" without letting the comparable set collapse to zero.
### The matching hierarchy
1. **Same make, model and registration year.** The non-negotiable core.
2. **Mileage within a band** — proportional rather than absolute. ±15% is more sensible than ±20,000 km, which is a rounding error on a 200,000 km car and a different vehicle on a 30,000 km one.
3. **Same fuel and gearbox.** These are large, discrete price effects and should never be pooled.
4. **Same damage state.** Never compare a damaged car to a clean one. This is the single biggest source of nonsense comparables.
5. **Recency window** — 60 to 90 days is usually the right trade-off. Shorter and you have no sample; longer and you are averaging across a market that has moved.
### When the sample is too small
It frequently will be. Relax constraints in a deliberate order rather than all at once, and surface which ones you relaxed:
- First widen the **mileage band** — the cheapest relaxation, and mileage is the easiest variable to adjust for.
- Then widen the **year range** to ±1, adjusting for depreciation.
- Then widen the **geography**, accepting that cross-border price levels differ.
- **Never** relax fuel, gearbox or damage state. A comparable set that mixes those is not a comparable set.
And show the sample size. A valuation built on four observations and one built on four hundred should not look identical to the user. Confidence is information, and hiding it is how a price estimate becomes a liability.
## Reference prices in practice
The `/api/top-offers` endpoint applies this logic to find lots priced below their reference, and — importantly — returns the reference with the lot rather than just a verdict:
```json
{
"auction_id": 11125938,
"site_name": "openlane",
"public_price_eur": 9800,
"market_reference": {
"price_eur": 12400,
"mileage": 71000,
"km_difference": -6205,
"explanation": "Reference from comparable listings, adjusted for mileage difference"
}
}
```
_A deal signal you cannot inspect is a black box. The reference price, the mileage it was drawn at, and the adjustment are all returned so the number can be argued with._
That transparency matters more than the accuracy of any single estimate. Users will find cases where a signal looks wrong, and being able to show them the comparable set turns a credibility problem into a conversation.
## Wholesale is only half the picture
Auction history tells you what the trade pays. It does not tell you what the car retails for, and the spread between those two is where the margin lives. Retail asking prices come from classifieds, which is a separate dataset with separate semantics — an asking price is not a transaction price, and the gap between them varies by model and by how long the ad has been running.
[Used car market value and comparables](/blog/used-car-market-value-comparables) covers combining the two properly, including why asking prices need discounting before you compare them to hammer prices.
## Start recording now
The recurring theme: history is the one dataset that cannot be acquired retroactively. If price intelligence is anywhere on your roadmap — even eighteen months out — the archive needs to exist by then, which means either starting collection today or choosing a provider that already has it. Deciding later is deciding not to have it.
### FAQ
**What is a car price history API?**
An endpoint returning the sequence of observed price changes for a specific auction lot over its lifetime — opening price, bid movements, buy-now changes and the final clearing figure — each with a timestamp and, ideally, an indication of which fields changed so a currency movement can be distinguished from a genuine price change.
**Can I use current bids as market value?**
No. A current bid below an unmet reserve is a floor, not a value — the car is not available at that price. Models trained on unmet-reserve current bids as if they were sale prices systematically underestimate the market, and the error is invisible in aggregate quality metrics. Weight final clearing prices instead.
**How far back should price history go for valuation?**
For comparables, 60 to 90 days is usually the right window — shorter leaves you without a sample, longer averages across a market that has moved. For detecting seasonal patterns or year-over-year depreciation you need at least two full years, which is why the archive depth of a provider matters more than most buyers realise at evaluation time.
**Why do two providers disagree about what a car is worth?**
Almost always the comparable set, not the arithmetic. Different mileage bands, different recency windows, different handling of damaged vehicles, different treatment of relisted lots, and different decisions about whether unsold lots count as observations all move the answer. A provider that shows you the comparable set is more useful than one that shows a more confident number.
**Should relisted vehicles be counted twice?**
No. A car that failed to sell and was relisted appears as two lots but is one vehicle, and counting both over-weights exactly the cars the market rejected. Deduplicate on VIN where it is published, and on a specification-plus-mileage signature where it is not — then flag the relist, because the fact that it failed to sell the first time is itself signal.
---
# Encar data: reading the Korean used car market in English
URL: https://thecarapi.com/blog/encar-korean-used-car-data
Published: 2026-04-29 · Updated: 2026-08-07
Category: Sources
Korean supply is the most underused dataset in European vehicle sourcing, mostly because of a language barrier that is a solved problem.
Encar is the dominant used-vehicle marketplace in South Korea, and South Korea is a significant net exporter of used cars. For European buyers, exporters and marketplace operators, that combination makes it one of the more interesting supply pools available — and one that most tooling ignores, largely because the listings are in Korean.
This covers what is actually in the data, the ways the Korean market differs from European wholesale in ways that affect your code, and how to work with it in English.
## Why the Korean market behaves differently
Four structural properties, each of which has a consequence for anyone modelling the data.
### Inspection culture
Korean used-vehicle sales operate with a strong emphasis on documented inspection and disclosed accident history. Listings routinely carry structured condition information at a level of detail that is unusual by European wholesale standards. For a data consumer this is a genuine advantage: the condition signal is richer, and it is more consistently present.
### Odometer reliability
Mileage disclosure norms in the Korean market are widely regarded as strong. That does not make individual readings guaranteed, and normal diligence still applies — but as a population, Korean mileage data has less of the systematic distortion that makes European odometer figures a modelling hazard.
### Specification divergence
This one will bite you. Korean-domestic-market trims frequently do not correspond to European trim names for the same nameplate. A Korean-market Sonata or K5 may have equipment combinations, engine options and trim labels that simply have no European equivalent. Any model catalog built purely from European listings will fail to match a meaningful share of Korean stock.
### Third-country status
For an EU buyer, Korea is outside the customs union. Import duty and import VAT apply, on top of shipping that takes weeks rather than days. Landed cost is not a small adjustment to the listed price — it is a different calculation entirely, and it is why fee modelling has to be per-source rather than one formula. See [the import calculator endpoints](/docs/calculator).
> **Do not reuse EU landed-cost logic for Korean stock** — Applying an EU-internal cost model — no duty, VAT already accounted for — to a Korean vehicle understates the final bill substantially. If your product shows a landed price, the source has to drive which model runs.
## The language problem, and what solving it means
Encar listings are written in Korean. Machine-translating them at read time is the obvious approach and the wrong one, for reasons that are worth spelling out:
- **Vehicle terminology translates badly out of context.** Trim names, equipment codes and damage terms are domain jargon, and general-purpose translation mangles them in ways that look plausible.
- **It is not idempotent.** Translate on every read and the same field yields slightly different English on different days, so your filters and your cached values disagree.
- **It is expensive at volume** and adds latency to every request.
- **It cannot be indexed.** You cannot search or facet on text that only exists after a translation call.
Translation belongs at ingest, once, with the result stored as a first-class field and the Korean original retained. That gives you something searchable, stable and auditable:
```json
{
"auction_id": 38112900,
"site_name": "encar",
"car_name_en": "BMW 320d xDrive Touring M Sport",
"clean_make": "BMW",
"clean_model": "320d",
"registration_year": 2020,
"mileage": 45000,
"fuel_group": "Diesel",
"gearbox_group": "Automatic",
"public_price_eur": 21500,
"car_country_extended": "KR",
"car_identification": { "...": "original Korean payload, unmodified" }
}
```
_`car_name_en` is resolved at ingest and indexed. The Korean source payload stays in `car_identification` so a disputed translation can be checked against what the listing actually said._
## Querying Korean stock
Encar is the site slug `encar`, and it takes the same parameters as every other source:
```bash
# Korean stock, petrol automatics from 2019, under 60,000 km
curl -s "https://api.thecarapi.com/api/search?site=encar&fuel=Petrol\
&gearbox=Automatic&year_from=2019&kilometers_to=60000&sort=price_low&limit=24" \
-H "X-API-Key: $API_KEY"
# The Korean domestic marques carry the most volume
curl -s "https://api.thecarapi.com/api/search?site=encar&brand=hyundai&limit=24" \
-H "X-API-Key: $API_KEY"
```
> **Two useful filter habits for Korean stock** — Drive brand and model values from [the catalog endpoints](/docs/catalog) rather than hardcoding them — Korean-market nameplates include models never sold in Europe. And filter on `country` rather than assuming the source implies it, because normalized country is what your users actually mean.
## What Korean data is good for
Three uses where it earns its place rather than just adding rows:
1. **Arbitrage analysis.** The same model, the same year, priced in two disconnected markets. Comparing Korean listing prices against European retail asking prices from [the classifieds surface](/market-data) is the clearest form of this — but the comparison is only meaningful once landed cost is included.
2. **Supply for models that are scarce in Europe.** Certain hybrids and Korean-market specifications are far more available domestically than they are through European wholesale.
3. **Condition-rich training data.** The disclosure norms make Korean listings unusually good input for anything modelling the relationship between documented condition and price.
## Practical cautions
- **Currency and FX timing.** Prices originate in Korean won. Know whether the euro figure you are reading was converted at ingest or at read time, because on a volatile pair those differ.
- **Specification matching.** Budget for the fact that a European model catalog will not cleanly match all Korean trims. Fall back to the normalized model with the raw variant preserved rather than dropping unmatched rows.
- **Shipping duration is part of the price.** A car that arrives in two months carries two months of market movement and depreciation risk that a German car collected next week does not.
- **Right-hand drive is not an issue here** — Korea drives on the right and produces left-hand-drive vehicles, unlike Japanese domestic supply.
For the wider picture of how Encar fits alongside the European sources, [the sources page](/sources) has per-platform detail, and [the auction data guide](/blog/car-auction-api-guide) covers the cross-source fundamentals.
### FAQ
**Is there an Encar API for developers?**
Encar does not publish a general-purpose, self-serve English-language inventory API for third-party developers. Access to its data programmatically generally runs through a commercial arrangement or a data provider that maintains the integration. Verify current options directly with Encar if it is central to your product.
**How do you handle Korean-language listings?**
Translation belongs at ingest rather than at read time. Resolving an English vehicle name once and storing it as an indexed field makes it searchable, stable across reads, and cheap — whereas translating on every request is slow, non-idempotent, and produces text you cannot filter or facet on. The Korean original should be retained so translations can be audited.
**Is Korean used car mileage trustworthy?**
Korean market disclosure norms around odometer readings and inspection are widely regarded as strong, and as a population the data shows less systematic distortion than some European sources. That is a statement about the population, not a guarantee about any individual vehicle — normal verification still applies.
**Do Korean cars cost more to import into the EU than German ones?**
Yes, and materially. Korea is outside the EU customs union, so import duty and import VAT apply on top of sea freight that takes weeks. An EU-internal purchase from Germany incurs neither. Any landed-cost calculation has to branch on the source country — applying EU-internal logic to Korean stock will understate the final figure substantially.
**Are Korean cars left-hand drive?**
Yes. South Korea drives on the right and its domestic vehicles are left-hand drive, so the steering-side objection that applies to Japanese domestic stock does not apply here. Specification differences from European-market trims are the real matching challenge, not drive side.
---
# OpenLane auction data: coverage, lineage, and programmatic access
URL: https://thecarapi.com/blog/openlane-auction-data
Published: 2026-04-15 · Updated: 2026-08-07
Category: Sources
OpenLane is the remarketing side of the European wholesale market — fleet, lease and OEM returns rather than consumer trade-ins. That changes what the data is good for.
OpenLane operates as a digital remarketing platform in Europe, bringing together vehicle supply that had previously been distributed across several separately branded businesses. For anyone consuming the data, the useful thing to understand is not the corporate history but what kind of vehicles flow through it — because remarketing supply behaves very differently from consumer trade-in supply.
## Remarketing supply versus trade-in supply
Wholesale platforms broadly split by where their stock comes from, and it shows up clearly in the data.
| | Remarketing (fleet / lease / OEM) | Consumer trade-in |
| --- | --- | --- |
| Typical age | 2 – 5 years, tightly clustered | Wide spread, 1 – 15 years |
| Mileage | Higher, but predictable per year | Highly variable |
| Service history | Usually complete — contractually maintained | Patchy |
| Specification | Repeating fleet trims in volume | Effectively random |
| Condition | Consistent, with documented wear standards | Varies enormously |
| Batching | Often arrives in cohorts of identical cars | One-offs |
The cohort property is the one worth building around. When a leasing company returns forty identically specified Passats at the end of a contract, you get a set of near-duplicate vehicles that differ mainly in mileage and condition. For a pricing model that is unusually clean signal — it isolates the mileage and condition variables from everything else. For a marketplace frontend it is a UX problem, because forty near-identical results is a bad search page.
> **Detecting cohorts** — Group by make, model, registration year and a rounded specification signature, then look for clusters with a common first-seen date. Cohorts show up as a spike, and knowing about them lets you collapse them in search while keeping them all for modelling.
## A note on ADESA and CarsOnTheWeb
Two legacy names come up in searches: ADESA Europe and CarsOnTheWeb. Both are associated with the European remarketing business that now trades as OpenLane. We do not maintain historical lineage claims we cannot verify, and we would suggest treating any confident account of which brand became which — including in tools that surface old documentation — as something to check against OpenLane's own material rather than take at face value.
The practical implication for a developer is narrow but real: if you have historical data keyed on an old brand identifier, do not assume it maps cleanly onto current OpenLane lot ids. Verify before you join on it.
## What the listings carry
Remarketing listings are generally strong on documented condition, because the vehicles were inspected under a contractual standard rather than described by whoever is selling them. Expect the specification and history fields to be well populated, and the damage description to be structured around defined wear categories rather than free text.
That said, fill rates still vary by field and by consignor, and this is worth measuring rather than assuming. A field that is 95% populated for one consignor's stock and 40% populated for another's will average out to something misleading.
## Pulling OpenLane inventory
OpenLane sits under the site slug `openlane`, addressed identically to every other source:
```bash
# Recent low-mileage OpenLane stock in the Netherlands
curl -s "https://api.thecarapi.com/api/search?site=openlane&country=NL\
&year_from=2021&kilometers_to=80000&sort=price_low&limit=24" \
-H "X-API-Key: $API_KEY"
# Detail for one lot
curl -s "https://api.thecarapi.com/api/auction/openlane/11125938" \
-H "X-API-Key: $API_KEY"
# How the price on that lot has moved since we first saw it
curl -s "https://api.thecarapi.com/api/auction/openlane/11125938/price-history" \
-H "X-API-Key: $API_KEY"
```
Because remarketing stock arrives in cohorts, price history on OpenLane lots is more informative than on one-off consumer stock: you can watch how a set of near-identical vehicles clears, which is close to a controlled experiment in what the market will pay. [The price history guide](/blog/car-price-history-api) covers how to read that series properly.
## Where OpenLane fits against the other sources
Nobody should be sourcing from one platform. The reason to hold several is that they cover genuinely different supply:
- **OpenLane** — fleet and lease returns. Predictable, documented, cohorted. Best signal for pricing models.
- **Auto1** — broad consumer and mixed supply, largest volume, widest variety. See [the Auto1 access options](/blog/auto1-inventory-data-access).
- **Schadeautos** and **Copart Germany** — damaged and salvage, a different buyer and a different funnel entirely.
- **eCarsTrade** — ex-lease and ex-rental fleet stock, overlapping with OpenLane but with its own consignor base.
- **Encar** — Korean domestic supply, a separate market with separate price dynamics.
A query filtered to one source answers "what is available here". A query across all of them answers "what is this car worth right now", which is the question with commercial value. That comparison only works if the sources have been normalized onto one schema first — see [vehicle data normalization](/blog/vehicle-data-normalization) for why that is harder than it sounds.
### FAQ
**Does OpenLane offer a public API?**
OpenLane operates as a professional remarketing platform with access oriented around registered trade accounts, and does not publish a general-purpose self-serve inventory API for arbitrary developers. Where programmatic access exists it is a commercial arrangement. Confirm current options directly with OpenLane for your specific use case.
**What is the difference between OpenLane and Auto1 inventory?**
Mainly the source of supply. OpenLane is weighted towards fleet, lease and OEM returns — vehicles two to five years old with documented service history, often arriving in cohorts of identical specification. Auto1 carries broader mixed supply including consumer-sourced vehicles, with far more variety in age, mileage and condition.
**Are ADESA Europe and CarsOnTheWeb the same as OpenLane?**
Both names are associated with the European remarketing business now trading as OpenLane, but we do not publish a definitive lineage we have not verified. If you hold historical data keyed on a legacy brand identifier, verify against OpenLane's own material before assuming those identifiers map onto current lot ids.
**Why do I see many near-identical vehicles in OpenLane results?**
End-of-contract fleet returns arrive in cohorts — dozens of identically specified cars entering the market together. For pricing models this is excellent signal because it isolates mileage and condition from specification. For a search UI it is a problem worth solving by collapsing cohorts into a single result with a count.
---
# Auto1 inventory data: what exists, what does not, and the options
URL: https://thecarapi.com/blog/auto1-inventory-data-access
Published: 2026-03-30 · Updated: 2026-08-07
Category: Sources
The search that brings most people here is "Auto1 API". The honest answer is that there is no public self-serve one — which makes the next question the interesting one.
Auto1 Group operates the largest pan-European wholesale vehicle marketplace. If you are building anything that needs a broad view of European used-car wholesale supply, its inventory is not optional — it is the reference set that everything else gets compared against.
Which is why "Auto1 API" is a common search, and why the result is usually disappointing. This article sets out what is actually available, why, and what the realistic paths are.
## What Auto1 is, structurally
Auto1 is a closed B2B marketplace. Vehicles are sourced largely from consumers and fleets, inspected, and then offered to a network of registered professional buyers across Europe. The consequence for anyone wanting the data is that the marketplace is designed around verified dealer accounts, not around open access.
- Listings are aimed at professional buyers, with condition reports and inspection detail rather than consumer marketing copy.
- Access is tied to a registered trade account, typically requiring a verifiable business.
- Pricing conventions are wholesale: net figures, buyer fees layered on top, and margin-scheme VAT treatment that differs from retail.
- Stock turns over quickly — the lifespan of an individual lot is short, which makes freshness the dominant technical requirement.
## Is there a public Auto1 API?
Not a general-purpose, self-serve, publicly documented one that a developer can sign up for and start calling. That is the state of affairs as far as public documentation goes, and it is consistent with the marketplace being closed by design.
> **Verify this yourself before you build** — Platform access programmes change, and terms differ by country and account type. Anything you read in an article — this one included — is a snapshot. If Auto1 data is central to your product, ask Auto1 directly what is available under your specific commercial arrangement.
There are two things that sometimes get mistaken for a public API and are worth naming so you do not waste a week on them. Internal endpoints that a logged-in web session calls are not a published interface: they are undocumented, unversioned, session-bound, and change without notice. Partner or integration APIs, where they exist, are commercial arrangements with contractual terms rather than something you self-serve.
## Why the closed model makes technical sense
It is easy to read this as gatekeeping. The structural reasons are more mundane, and understanding them tells you what any access route will look like.
1. **Wholesale pricing is commercially sensitive.** Trade buy prices published openly would undermine both the sellers' position and the platform's.
2. **The buyer network is the product.** Verified dealers are what makes the marketplace liquid; open access dilutes the thing being sold.
3. **Listing photography and condition reports are owned assets** that the platform invests in producing.
4. **Regulatory exposure.** Fleet and consumer-sourced vehicles carry provenance data that the platform is accountable for.
## The realistic routes to the data
Three, with genuinely different trade-offs.
### 1. Direct commercial arrangement with Auto1
If you are a large buyer or a strategic partner, this is the cleanest route — first-party data, first-party terms, no intermediary. It requires commercial standing and a procurement process, and it gets you exactly one source. For teams whose product needs a cross-platform view, that last point is the limitation.
### 2. Collect it yourself
Technically possible and legally and operationally non-trivial. Auto1's buyer-facing content sits behind authentication, which changes the character of the exercise significantly compared to scraping a public page — you are operating an account under its terms of service. The maintenance burden is covered in [the cost breakdown](/blog/scraping-car-auction-sites-vs-api); the additional consideration here is that access is tied to an account that can be withdrawn, which makes it a fragile foundation for a commercial product.
### 3. A multi-source data provider
The route that exists because of the gap between one and two. A provider maintains the source relationships and pipelines, and exposes Auto1 inventory alongside other platforms under one schema and one key. You trade first-party directness for coverage breadth and for not owning the maintenance.
| | Direct with Auto1 | Collect yourself | Data provider |
| --- | --- | --- | --- |
| Sources covered | Auto1 only | What you build | Multiple, one schema |
| Time to first data | Procurement cycle | Weeks to months | Under an hour |
| Ongoing maintenance | Low | **High, permanent** | None |
| Historical archive | Depends on terms | Starts from day one | Available immediately |
| Access stability | Contractual | Account-dependent | Contractual |
| Cross-source comparison | Not possible | You build it | Built in |
## What Auto1 data looks like normalized
On our side Auto1 is one of seven auction sources, reachable under the site slug `auto1` and addressed by the same `site/auction_id` pair as everything else:
```bash
# Auto1 stock only, diesel, from 2018, cheapest first
curl -s "https://api.thecarapi.com/api/search?site=auto1&fuel=Diesel\
&year_from=2018&sort=price_low&limit=24" \
-H "X-API-Key: $API_KEY"
# One lot, full normalized detail plus the raw source payload
curl -s "https://api.thecarapi.com/api/auction/auto1/1313664441" \
-H "X-API-Key: $API_KEY"
# Gallery, re-hosted as WebP on CDN so it survives the lot closing
curl -s "https://api.thecarapi.com/api/auction-images/auto1/1313664441" \
-H "X-API-Key: $API_KEY"
```
The point of the shared shape is that swapping `site=auto1` for `site=openlane` changes the source and nothing else about your code. Cross-source comparison — the same model on Auto1 versus OpenLane versus Copart Germany — is the capability that no single-source route can give you.
> **German stock, and why that matters for import maths** — Auto1's centre of gravity is German and broadly EU-internal, which means no third-country import duty and VAT already accounted for within the union. That is a materially different landed-cost calculation from Korean or Japanese sourcing, and it is why the [import calculator](/docs/calculator) models fees per source rather than applying one formula.
## Choosing between the three
A short decision rule that holds up in practice:
- You are a **major buyer** and Auto1 is effectively your only supply channel → go direct. Nothing beats first-party.
- You need **one narrow slice** — one brand, one country, low volume → a small in-house collector may be proportionate, with the account-dependency caveat understood.
- You are building a **product** — a marketplace, a pricing model, an exporter tool — that needs more than one source → a provider is almost always the right call, because the value is in the comparison and no single source can produce it.
For the equivalent picture on the other major European platform, see [OpenLane auction data](/blog/openlane-auction-data). For the full list of what we cover and how each source behaves, [the sources page](/sources) has the detail.
### FAQ
**Does Auto1 have a public API?**
There is no general-purpose, publicly documented, self-serve inventory API that a developer can sign up for. Auto1 operates a closed B2B marketplace built around verified dealer accounts. Where programmatic access exists it is a commercial arrangement rather than a public product. Access programmes change, so verify directly with Auto1 for your specific case.
**Can I use Auto1's internal web endpoints?**
The endpoints a logged-in web session calls are not a published interface. They are undocumented, unversioned, bound to a session, subject to the account's terms of service, and can change without notice. Building a commercial product on them means building on something that can break or be withdrawn at any time.
**How do I get Auto1 inventory data programmatically?**
Three routes: a direct commercial arrangement with Auto1, which is cleanest but covers one source; collecting it yourself, which is high-maintenance and account-dependent; or a multi-source data provider that already maintains the pipeline and exposes Auto1 alongside other platforms under one schema. The right choice depends mainly on whether you need cross-source comparison.
**Is Auto1 data comparable with other auction platforms?**
Not without normalization. Auto1 uses its own model naming, its own fuel and gearbox vocabulary, and wholesale pricing conventions — net figures with buyer fees layered on top — that differ from other platforms. Comparing raw figures across sources produces wrong answers; the prices must first be restated on one consistent basis.
**Why does Auto1 inventory turn over so fast?**
It is a wholesale marketplace with short auction cycles rather than a retail listing site, so an individual lot has a brief life. That makes sync freshness the dominant technical requirement for anyone consuming the data — a feed refreshed hourly will show a meaningful share of lots that have already closed.
---
# Vehicle data normalization: turning seven auction feeds into one schema
URL: https://thecarapi.com/blog/vehicle-data-normalization
Published: 2026-03-14 · Updated: 2026-08-14
Category: Data engineering
Normalization is where multi-source vehicle projects succeed or quietly fail. The failure mode is not a crash; it is a filter that returns the wrong cars and nobody can tell.
Seven auction platforms describing the same car will produce seven different strings, three different fuel vocabularies, two currencies, and at least one field where the same name means something different from everywhere else. Getting them into one schema is the work. It is also the work that gets underestimated by roughly an order of magnitude, because every individual mapping looks trivial and there are several thousand of them.
This is a practical account of the problems, in the order you will hit them.
## Start with the fields that look easy
Fuel type is the canonical example of a field everyone assumes is a three-value enum. In practice, across European wholesale sources, you will encounter all of the following as distinct raw values meaning overlapping things:
```text
Petrol · Gasoline · Benzin · Essence · Benzina · Gasolina
Diesel · Gasoil · Gazole · Nafta
Hybrid · Hybrid (Petrol) · HEV · Full Hybrid · Mild Hybrid · MHEV
Plug-in Hybrid · PHEV · Plug-in Hybrid (Diesel) · Hybrid Rechargeable
Electric · BEV · Elektro · Électrique · Elettrica
LPG · GPL · Autogas · Petrol/LPG · Bi-fuel
CNG · Erdgas · Methane · Petrol/CNG
```
The mapping itself is straightforward. The decisions inside it are not, and they are product decisions rather than engineering ones:
- Does a **mild hybrid** belong in `Hybrid` or in `Petrol`? A 48V MHEV drives like a petrol car and is taxed like one in most markets, but a buyer filtering for "Hybrid" may well expect to see it.
- Does **plug-in hybrid** collapse into `Hybrid`, or is it its own group? Buyers who want a PHEV specifically do not want an HEV, and the price difference is substantial.
- Is **bi-fuel petrol/LPG** filed under `Petrol` or `LPG`? It is genuinely both.
> **Decide once, write it down, expose both** — Whatever you choose, the resolution is the same: keep a canonical `fuel_group` for filtering and retain the original source string alongside it. Users filter on the group; anyone who needs the distinction can read the raw value. Never overwrite the source value in place.
## Make, model and the trim problem
Make is nearly tractable — a few hundred values, mostly with obvious aliases (`VW` / `Volkswagen`, `Mercedes` / `Mercedes-Benz` / `MB`, `Alfa` / `Alfa Romeo`). Even here there are traps: `DS` was a Citroën trim before it became a marque, and listings from before the split still file DS cars under Citroën.
Model is where it becomes genuinely hard, because sources disagree about where the model ends and the trim begins.
| Source string | Model | Trim / variant | Note |
| --- | --- | --- | --- |
| `BMW 320d Touring xDrive M Sport` | 3 Series | 320d Touring xDrive M Sport | Engine code is load-bearing — 320d ≠ 320i |
| `VW Golf VII 1.6 TDI Comfortline` | Golf | 1.6 TDI Comfortline | Generation number matters for valuation |
| `Mercedes C 220 d T-Modell AMG Line` | C-Class | C 220 d Estate AMG Line | `T-Modell` is German for estate, not a trim |
| `Peugeot 3008 1.5 BlueHDi Allure Pack` | 3008 | 1.5 BlueHDi Allure Pack | Numeric model names collide with engine sizes |
| `Kia Niro 1.6 GDi HEV` | Niro | 1.6 GDi HEV | Same nameplate exists as HEV, PHEV and BEV |
The Kia Niro row is the instructive one. `Niro` is a single nameplate covering a hybrid, a plug-in hybrid and a full electric car with completely different powertrains, weights and prices. Normalize to the nameplate alone and you produce a "model" whose price distribution is trimodal and whose average is meaningless.
### The workable structure
Three fields rather than two, populated with decreasing confidence:
1. `clean_make` — matched against a curated marque list with aliases. High confidence, near-total coverage.
2. `clean_model` — matched against a per-make model catalog. Good confidence; needs manual curation for new nameplates.
3. **Raw variant string** — preserved verbatim. Do not attempt to parse trim into structured fields unless a customer is paying for it; the combinatorics are brutal and the payoff is small.
## Prices are not numbers
A price field is the most dangerous field in the payload, because it is a number and numbers look trustworthy. Before comparing two prices from different sources, you need to know six things about each:
- **Currency**, and for non-euro sources, the FX rate and *when* it was applied. A Korean won price converted at ingest is not the same figure as one converted at read time.
- **VAT treatment** — inclusive, exclusive, or margin-scheme. This is a 20%+ swing and sources genuinely differ.
- **Fee inclusion** — does the displayed figure include the platform's buyer fee, which is often a non-linear function of the hammer price?
- **Price type** — current bid, buy-now, reserve, or estimate. These are not interchangeable and a single `price` column that mixes them is unusable.
- **Reserve state** — a current bid below an unmet reserve is not a price at which the car is available.
- **Observation time** — on a live lot, a price without a timestamp is not a fact.
The resolution is a comparable field computed on one consistent basis, sitting next to the untouched source figure:
```json
{
"current_price": 9800, // source figure, source currency, source basis
"currency": "EUR",
"public_price_eur": 11466.00, // one consistent basis, comparable across sources
"buy_now_price": 12400,
"buy_now": true
}
```
_Filter and sort on the comparable field. Display the source figure when the user is looking at the lot on its own. Never sort on a mixed-basis column._
> **The cheapest car in your search is probably a bug** — When a price-ascending search returns something implausible, the cause is almost always basis mixing — a net-of-VAT figure sorted against gross ones, or a stale FX rate on a non-euro source. Check the basis before you check the vehicle.
## Mileage, and the units trap
Kilometres nearly everywhere in Europe, miles for UK and Irish stock, and occasionally an unlabelled integer whose unit you infer from the source. Two additional wrinkles worth handling explicitly:
- Some sources publish mileage in thousands. A `45` in a field that usually holds `45000` is not a 45 km car; it is a units bug waiting to reach a customer.
- Zero and null are different. Zero means the odometer read zero — plausible on a delivery-mileage car, suspicious otherwise. Null means the source did not say. Collapsing them makes every unknown-mileage car look like a new one.
## Damage: the field with no shared vocabulary
There is no standard European damage taxonomy. A salvage-focused source may publish structured per-panel damage; a general wholesale source may publish a single free-text sentence; another may publish nothing but a boolean. Attempting to unify them into one rich schema produces a schema that is mostly null.
The pragmatic layering that survives contact with real data:
1. A **boolean** that is reliably populated for every source — damaged or not. Coarse, but it is the filter 90% of users actually want.
2. A **severity band** where the source supports it — light, structural, total loss.
3. The **raw damage payload**, unmodified, for the sources that publish detail. Anyone doing repair estimation reads this and nothing else.
Resist the urge to synthesise severity from free text with a heuristic. It will be wrong on the edge cases, and the edge cases are exactly the expensive cars. [Damaged and salvage vehicle data](/blog/salvage-damaged-vehicle-data) goes into this in more depth.
## Keep the raw payload. Always.
If there is one thing to take from this article: normalization is lossy, your mapping will be wrong somewhere, and you will find out from a customer. Retaining the untouched source payload turns that from an outage into a lookup.
```bash
curl -s "https://api.thecarapi.com/api/auction/schadeautos/1775437" \
-H "X-API-Key: $API_KEY" | jq '.car_identification'
```
_Every normalized vehicle keeps `car_identification` — the source payload exactly as published, so a disputed field can be traced to what the source actually said._
It also makes normalization improvable. When you fix a mapping, you can reprocess history from the raw payloads instead of waiting for the corrected data to accrue going forward.
## Validate continuously, not at ingest
Schema validation at ingest catches type errors. It does not catch the failures that matter, which are distributional. Four monitors worth running on a schedule:
- **Fill rate per field per source.** A field that was 95% populated and drops to 60% overnight is a broken extractor, whatever the logs say.
- **Unmapped-value alerts.** Any raw fuel, gearbox or body value that fails to map should raise, not silently fall into an `Other` bucket where it disappears.
- **Price distribution shift.** A source whose median price moves 30% in a day changed its VAT basis or its currency. It did not suddenly acquire cheaper cars.
- **Cross-source sanity.** The same make and model should have overlapping price ranges across sources. Non-overlap is a normalization bug, not a market insight.
> **Build the vocabulary endpoints first** — Expose the canonical values as data — brands, models, fuels, gearboxes, countries — rather than baking them into your frontend. It makes the vocabulary auditable, lets clients discover new values automatically, and means a new source adds options instead of breaking filters. See [the catalog endpoints](/docs/catalog).
## The order to build in
If you are starting this from scratch, the sequence that avoids the most rework:
1. Store raw payloads first, before you normalize anything. Everything else can be recomputed from them; nothing can be recovered without them.
2. Normalize make, then fuel, then gearbox. Highest value, lowest ambiguity.
3. Build the comparable price field early — it gates every meaningful filter and sort.
4. Model matching next, accepting that it needs ongoing curation forever.
5. Damage last, and layered rather than unified.
6. Add the distributional monitors before you have customers, not after your first bad-data incident.
If this reads like more work than you want to own, that is a reasonable conclusion — it is roughly the argument in [the build-versus-buy breakdown](/blog/scraping-car-auction-sites-vs-api), and [the data dictionary](/docs/schema) documents the schema this article describes as it actually ships.
### FAQ
**What does vehicle data normalization mean?**
Mapping the varied, source-specific ways different platforms describe a vehicle — make, model, fuel, gearbox, price basis, damage — onto one consistent schema, so that data from multiple sources can be filtered, sorted and compared together. The defining constraint is that it must be lossy without being destructive: the canonical values sit alongside the original source values, never replacing them.
**Should mild hybrids be classified as hybrid or petrol?**
There is no universally correct answer — it is a product decision. A 48V mild hybrid drives and is taxed like a petrol car, but users filtering for "hybrid" may expect to see it. Whichever you choose, keep the original source string so the distinction is recoverable, and document the decision where your API consumers will read it.
**Why can I not just use VIN to match vehicles across sources?**
VIN is the ideal key but is sparsely published in wholesale auction listings — many platforms reveal it only to logged-in dealers, and fill rates vary widely by source and country. An architecture that assumes VIN as the join key works on test data and fails in production. Use it when present, and have a normalized-attribute fallback for when it is not.
**How do you compare prices from sources with different VAT treatment?**
Compute a single comparable field on one consistent basis — a common currency, a defined VAT treatment, and a stated fee inclusion policy — and store it alongside the untouched source figure. Filter and sort on the comparable field only. Sorting a column that mixes net and gross figures is the most common cause of implausible results in price-ascending search.
**How do you detect that normalization has silently broken?**
Distributional monitoring rather than schema validation. Watch per-field fill rates by source, alert on raw values that fail to map instead of bucketing them into "Other", flag sudden shifts in a source's median price, and check that the same make and model has overlapping price ranges across sources. Type validation passes while all four of these are failing.
---
# Scraping car auction sites: the real cost, versus buying the data
URL: https://thecarapi.com/blog/scraping-car-auction-sites-vs-api
Published: 2026-02-26 · Updated: 2026-08-14
Category: Build vs buy
The build-versus-buy conversation usually stalls because one side is comparing a subscription against a sprint. The honest comparison is against three years of maintenance.
Someone on your team has already built a scraper for one auction site. It took a weekend, it worked, and it produced a CSV that made everyone briefly optimistic. That prototype is the reason this conversation is difficult: it is real evidence that the problem is easy, and it is measuring the wrong thing.
The weekend prototype solves collection for one source, one page layout, one moment in time, with no availability requirement. Production solves collection for seven sources, continuously, while the sources actively change, at a volume where storage and bandwidth stop being rounding errors. This article prices that second thing.
## The cost model, line by line
Figures below are order-of-magnitude for a team collecting roughly a million live listings across seven European and overseas sources with images. Your numbers will differ; the structure of the model will not. Treat the ranges as a framework to fill in with your own quotes, not as a benchmark.
| Line item | Typical monthly range | Why it lands where it does |
| --- | --- | --- |
| Residential / mobile proxies | €800 – €4,000 | Datacentre IPs get blocked quickly on these targets. Residential bandwidth is priced per GB, and image fetching dominates the bill. |
| Anti-bot / CAPTCHA solving | €150 – €900 | Only needed on some sources, but the ones that need it need it constantly. |
| Compute (crawlers, browsers, queues) | €300 – €1,500 | Headless browsers are ~10× the cost of plain HTTP fetches. Which sources need one drives the whole figure. |
| Object storage for images | €100 – €600 | Multiple terabytes once you keep galleries for closed lots. Cheap per TB, easy to forget entirely. |
| CDN / egress for images | €200 – €1,200 | Scales with your traffic, not your crawl. Frequently the line that surprises people at launch. |
| Database and search | €200 – €800 | Faceted search over a million rows with price ranges is not a small instance. |
| Engineering maintenance | **0.5 – 1.5 FTE** | The dominant cost by a wide margin, and the one that never appears in the initial estimate. |
_Infrastructure lands somewhere around €2k–€9k/month. The engineering line is usually larger than everything above it combined._
> **The line that gets left off** — Half an engineer, permanently, is not a rounding error — at European loaded cost that is roughly €40k–€70k a year, every year, forever. It is also half an engineer not building the thing your customers actually pay for.
## Why maintenance never converges
The intuition that scrapers stabilise over time is wrong, and it is wrong for a specific reason: you are not maintaining code against a fixed specification. You are maintaining it against seven independent teams who ship whenever they like and owe you nothing.
### Markup drift
A frontend redesign, an A/B test, or a component library upgrade breaks selectors. The A/B test case is the worst: your extraction succeeds for 70% of requests and silently returns nulls for the other 30%, which looks like sparse data rather than a bug and can run for weeks before anyone notices.
### Anti-bot escalation
Bot mitigation improves continuously and asymmetrically. Every provider upgrade is a step change for you, arrives without notice, and is followed by an urgent unplanned week. You cannot schedule around it and you cannot predict it.
### Semantic drift
The subtlest failure. A source renames a fuel category, changes a currency, starts including VAT where it previously excluded it, or adds a fee to a displayed price. Nothing errors. Your pipeline ingests the new meaning under the old field name, and every downstream number is quietly wrong. This is the class of bug that reaches customers.
> Broken scrapers announce themselves. Semantically drifted scrapers do not, and those are the ones that damage trust in your product.
### Coverage decay
Rate limits force you to prioritise. You start crawling only the first N pages of each source, or refreshing prices only for lots under a certain age. Coverage drops from 99% to 85% over a year without a single incident, and nobody notices until a customer asks why a car they can see on Auto1 is not in your product.
## The legal and contractual dimension
This is genuinely jurisdiction-dependent and this article is not legal advice — but it belongs in the decision, because it is a real risk that rarely makes it into the spreadsheet.
- Most auction platforms' terms of service restrict automated access. Whether those terms bind you, and with what consequence, varies by country and by how you access the site.
- The EU **Database Directive** creates a *sui generis* right over substantial investment in compiling a database, separate from copyright in the individual records.
- Photographs are copyrighted works. Re-hosting a source's images is a distinct question from extracting factual specifications, and the answer is not the same.
- Personal data occasionally appears in listing text — seller names, phone numbers, locations — and GDPR applies to it regardless of how you obtained it.
The practical point is not that collection is forbidden. It is that the risk is real, unquantified, and sits with you. Buying from a provider does not make the underlying questions disappear, but it does move the operational relationship with the sources onto someone whose business depends on maintaining it, and it gives you a contract to point at.
## Time to first useful product
Cost is one axis. Calendar time is usually the one that decides it.
| Milestone | Building in-house | On an API |
| --- | --- | --- |
| First listing in your database | Days | Under an hour |
| One source, reliable, with images | 3 – 6 weeks | Same hour |
| Seven sources, normalized to one schema | 4 – 8 months | Same hour |
| Historical archive worth modelling on | **12+ months of collection** | Available immediately |
| Steady state | Never — see maintenance | Provider's problem |
_The archive row is the one that cannot be bought back with money or headcount. History only accrues in real time._
That last row deserves emphasis. If your product needs price history — comparables, market value estimation, anything trained on past sales — then starting collection today means your first credible model is a year away. There is no way to accelerate it, because the data is generated by the passage of time.
## When building it yourself is the right answer
Not a rhetorical section. There are cases where in-house collection is clearly correct, and pretending otherwise would be dishonest.
- **One source, one narrow slice.** You need Copart Germany, salvage only, one brand. That is a maintainable script, not a platform.
- **Data collection is your product.** If you sell the feed, the pipeline is your moat and outsourcing it makes no sense.
- **You need a field nobody exposes.** A specific inspection-sheet annotation, a seller-level signal — if it is not in any commercial feed, you have no alternative.
- **A source no provider covers.** Regional platforms outside the major seven are often only reachable if you build it.
- **Contractual data residency** requirements that a third-party API cannot satisfy.
> **The hybrid that usually wins** — Buy the commodity — the seven large sources everyone needs, normalized, with images — and spend your engineering budget on the one proprietary source or derived signal that actually differentiates you. Nobody wins a market by having the same Auto1 listings as everyone else, slightly later.
## Making the comparison honestly
If you are running this decision internally, build the comparison over 36 months rather than 12, and include these four things that standard estimates omit:
1. Loaded engineering cost for ongoing maintenance, not just the initial build. Use a real fully-loaded figure, not salary.
2. The opportunity cost of that engineering time — what does not get built.
3. The revenue impact of coverage gaps and stale prices during the months your pipeline is degraded.
4. The value of the archive you will not have for the first year.
Run that model and the answer is usually clear in one direction or the other, which is the point. For a concrete comparison of the two approaches against our own surface, [see the side-by-side](/vs-diy-scraping); for what the data looks like once it arrives, start with [the auction data guide](/blog/car-auction-api-guide).
### FAQ
**How much does it cost to scrape car auction sites at scale?**
Infrastructure for roughly a million live listings across seven European and overseas sources with images typically runs €2,000–€9,000 per month — proxies and image egress dominate. The larger cost is engineering: sustained maintenance is generally 0.5–1.5 full-time engineers, which at European loaded cost exceeds the entire infrastructure bill.
**Is scraping car auction websites legal?**
It depends on jurisdiction, on the platform's terms of service, and on what you extract and republish. In the EU the Database Directive's sui generis right, copyright in listing photographs, and GDPR where listings contain personal data are all separate considerations from the terms of service. This is a question for a lawyer familiar with your jurisdiction and business model, not one to settle from a blog post.
**Why do auction scrapers break so often?**
Four independent causes: markup changes from redesigns and A/B tests, anti-bot escalation that arrives without notice, semantic drift where a field keeps its name but changes meaning, and gradual coverage decay under rate limits. Only the first announces itself with an error; the other three degrade data quality silently.
**Can I start with scraping and migrate to an API later?**
Yes, and it is a reasonable path for validating a market. The one thing that does not migrate is historical data — if your roadmap includes price modelling or comparables, the archive you have not been collecting cannot be bought back after the fact, so factor that into the timing of the switch.
**What is the biggest hidden cost people miss?**
Image bandwidth. Teams budget for collecting data and forget that each listing carries 20–60 photographs, that fetching them over residential proxies is billed per gigabyte, and that serving them to users is a second, separate egress bill that scales with traffic rather than with crawl volume.
---
# Car auction API: a practical guide to live vehicle auction data
URL: https://thecarapi.com/blog/car-auction-api-guide
Published: 2026-02-11 · Updated: 2026-08-07
Category: Fundamentals
Every team that builds on auction inventory hits the same three walls: identity, freshness, and images. Here is how the data actually behaves, and what to ask a provider before you sign.
A car auction API is a read interface over wholesale vehicle inventory: the lots that dealers, exporters and remarketing platforms trade between themselves before a car ever reaches a retail forecourt. It sounds like a solved problem — the listings are public, the pages render in a browser, the data is right there. In practice it is one of the messiest data domains in automotive, and the mess is structural rather than accidental.
This guide covers what the data looks like once it has been collected and cleaned, the specific failure modes that eat engineering months, and the questions worth asking any provider — including us — before you build a product on top of their feed.
## What auction inventory data actually contains
A useful mental model: every auction lot has four layers, and they refresh at completely different rates.
| Layer | What it holds | How often it changes |
| --- | --- | --- |
| Identity | Source platform, lot id, VIN when published, make, model, trim, year | Once, at listing time |
| Specification | Mileage, fuel, gearbox, power, body style, equipment, damage flags | Rarely — corrections only |
| Commercial | Current bid, buy-now price, reserve status, auction end time, fees | Continuously, sometimes per minute |
| Media | Photo galleries, condition reports, damage close-ups, inspection sheets | Once, then expires with the lot |
_Treating all four as one payload with one cache TTL is the single most common architecture mistake in this domain._
The commercial layer is why naive caching fails. A price you fetched forty minutes ago is not a price; it is a historical observation. Meanwhile the media layer is the opposite problem — galleries almost never change, but the source URLs frequently expire within days of the lot closing, so anything you did not copy is gone.
## The European auction landscape
The wholesale market in Europe is not one marketplace. It is a handful of large platforms with different access models, different geographic centres of gravity, and — critically — different ideas about what a "model" is.
- **Auto1** — German-headquartered, the largest pan-European wholesale platform by volume. Dealer-oriented.
- **OpenLane** — the remarketing platform behind what was historically ADESA Europe and CarsOnTheWeb. Strong in Benelux, France and Germany.
- **Schadeautos** — Dutch, specialised in damaged and salvage stock, with unusually detailed damage descriptions.
- **Copart Germany** — the German arm of the salvage auction group. Inside the EU customs union, which changes the duty maths against US-sourced salvage entirely.
- **eCarsTrade** — Belgian, ex-lease and ex-rental fleet stock sold in batches and singles.
- **Encar** — South Korea's dominant used-vehicle platform. High listing quality, honest odometer culture, and a market that exports heavily.
- **Japanese auctions** — the major domestic auction houses (USS, ARAI, AUCNET, BAYAUC, CAA) pooled into one source, with the auction inspection sheet, condition grade and steering side on every lot.
Each of these publishes a listing page. None of them publishes a general-purpose, self-serve inventory API for arbitrary third-party developers — access, where it exists at all, tends to run through commercial partnership or a dealer account with terms attached. Verify current terms directly with each platform before building; they change, and they differ by country.
> **Retail prices are a separate dataset** — Wholesale auction prices only tell you half the story. To know whether a lot is cheap you need the retail asking price for the same car in the same market. We keep those apart deliberately — see [the market intelligence surface](/market-data) — because merging classifieds into auction search produces results nobody can act on.
## Identity is the hard problem, not collection
Most teams assume the difficulty is fetching pages. It is not. The difficulty is answering "is this the same car?" across seven platforms that each invented their own taxonomy.
A concrete example. The same physical vehicle can be listed as `BMW 320d Touring xDrive`, `BMW 3-Series 320 d Touring`, `BMW SERIE 3 TOURING 320dA xDrive`, and `BMW 320` with the body style in a separate field. Group those by string equality and you get four models. Group them too aggressively and a 320d merges with a 320i, which is a different car at a different price.
VIN would solve this. VIN is also sparse — many wholesale listings publish it only to logged-in dealers, or not at all, and the fill rate varies enormously by source and by country. Any architecture that assumes VIN as a join key will work beautifully on your test set and fall over in production.
The workable answer is a normalization layer that produces stable canonical fields alongside the raw source values, so you can filter on the clean version and still audit the original:
```json
{
"auction_id": 38112900,
"site_name": "encar",
"car_name_en": "BMW 320d xDrive Touring M Sport",
"clean_make": "BMW",
"clean_model": "320d",
"registration_year": 2020,
"mileage": 45000,
"fuel_group": "Diesel",
"gearbox_group": "Automatic",
"public_price_eur": 21500,
"has_technical_damage": false,
"car_identification": { "...": "raw source payload, unmodified" }
}
```
_`clean_make` / `clean_model` / `fuel_group` are canonical across all sources. `car_identification` preserves the untouched source payload for when the normalization is wrong and you need to see why._
## Stable identifiers across sources
Auction lot ids are unique within a platform and meaningless across them. Two platforms will happily both have a lot `4471928`. The identity that survives is the pair:
```text
site_name + auction_id → encar/38112900
→ auto1/1313664441
→ openlane/11125938
```
Every detail, image and history endpoint should be addressable by that pair. If a provider gives you a synthetic internal id instead, ask what happens to it when they re-index a source — synthetic ids that churn on reindex will silently break every foreign key in your database.
## Freshness, and what a number like "3 minutes" means
Providers quote sync lag. The number is close to meaningless without knowing what it measures. Ask specifically:
1. Is that the lag for **new listings appearing**, or for **price changes on existing listings**? These are usually different pipelines with different budgets.
2. Is it a median or a worst case? A median of three minutes with a p99 of four hours is a very different product.
3. What happens to a lot when it **ends**? Does it disappear, get flagged, or silently freeze at its last observed price? A frozen sold lot that still reads as live will corrupt any pricing model you train on it.
4. Is the lag uniform across sources, or is one source refreshed hourly and averaged into a flattering headline figure?
> **The stale-lot trap** — If ended auctions are not explicitly marked, your "cheapest BMW 320d in Germany" query will eventually return a car that sold three weeks ago at a price nobody can get any more. Check that the API exposes an explicit lifecycle state and that filtering on it is the default, not an opt-in.
## Images are a bigger cost centre than the data
A wholesale listing carries 20 to 60 photographs. At a million live vehicles that is tens of millions of images, and they are the part of the payload your users actually look at. Three properties make them expensive:
- **They expire.** Source CDN URLs commonly stop resolving once a lot closes. If your product shows historical sales, hotlinking guarantees broken galleries.
- **They are hotlink-protected.** Many sources check `Referer` or require a session. Embedding source URLs directly in your frontend produces intermittent, hard-to-reproduce image failures.
- **They are unoptimised.** Original JPEGs at 3–6 MB each are fine for a browser on desktop broadband and catastrophic for a mobile listing grid.
The practical requirement is a vault: images copied at ingest, re-encoded to WebP, and served from your provider's CDN under URLs that outlive the lot. Ask whether the served URL is stable after the auction ends — that one question separates a media API from a link forwarder.
```bash
curl -s "https://api.thecarapi.com/api/auction-images/openlane/11125938" \
-H "X-API-Key: $API_KEY"
```
_Gallery endpoints are addressed by the same `site/id` pair as detail and price history._
## A first integration, end to end
The shape of a working integration is almost always the same: discover the filter vocabulary, search, then resolve detail and media only for what the user opens.
```bash
# 1. Learn the vocabulary — never hardcode brand or fuel strings
curl -s "https://api.thecarapi.com/api/brands" -H "X-API-Key: $API_KEY"
curl -s "https://api.thecarapi.com/api/fuels" -H "X-API-Key: $API_KEY"
# 2. Search — filter server-side, page with limit/offset
curl -s "https://api.thecarapi.com/api/search?brand=bmw&fuel=Diesel\
&year_from=2018&price_to=30000&sort=price_low&limit=24" \
-H "X-API-Key: $API_KEY"
# 3. Resolve detail only for the lot the user clicked
curl -s "https://api.thecarapi.com/api/auction/encar/38112900" -H "X-API-Key: $API_KEY"
# 4. Load the gallery lazily, after the detail view opens
curl -s "https://api.thecarapi.com/api/auction-images/encar/38112900" -H "X-API-Key: $API_KEY"
```
Two habits worth adopting from the start. First, drive your filter UI from the catalog endpoints rather than a hardcoded list — source vocabularies drift, and a hardcoded `Petrol` that becomes `Gasoline` upstream fails silently as an empty result set. Second, request `include_total=false` on infinite-scroll views; counting matching rows is frequently the most expensive part of a search and you rarely need the number.
## What to check before committing
A checklist that has saved more than one integration from a rewrite six months in:
- **Contract versioning.** Does every response carry a schema version, and is there a documented deprecation window? Silent field renames are the most common source of production breakage.
- **Request tracing.** Is there a request id in the response and in a header, so a support conversation can be about a specific call rather than a description of one?
- **Archive access.** Can you read lots that have already closed? Without history there is no price modelling, no comparables, and no way to evaluate whether a live lot is well priced.
- **Rate limits and pagination depth.** How deep can you page? A hard offset cap of a few thousand rows makes bulk reconciliation impossible and is often undocumented.
- **Feature gating.** Are search, detail, images and history all available on the entry plan, or does the useful half sit behind an enterprise tier you will only discover at scale?
- **Raw payload access.** When normalization gets something wrong — and it will — can you see what the source actually said?
## Where to go next
If you are weighing collecting this yourself against buying it, [the real cost breakdown of scraping auction sites](/blog/scraping-car-auction-sites-vs-api) puts numbers on both. If you have already decided to normalize multiple feeds in-house, [the normalization guide](/blog/vehicle-data-normalization) covers the taxonomy problems in detail. And [the API reference](/docs) documents every endpoint mentioned here, with live parameters.
### FAQ
**Is there an official public API for Auto1, OpenLane or Encar?**
None of the major European wholesale platforms publishes a general-purpose, self-serve inventory API for arbitrary third-party developers. Where programmatic access exists it typically runs through a commercial partnership or a dealer account with contractual terms. Terms differ by platform and by country and do change, so verify directly with each platform for your specific use case.
**How do you match the same vehicle across different auction platforms?**
VIN is the only truly reliable key, but it is sparsely published in wholesale listings, so it cannot be the sole join. The practical approach is a normalization layer producing canonical make, model, year, fuel and gearbox values, matched against a curated catalog, with the untouched source payload retained alongside so incorrect matches can be audited and corrected.
**What identifier should I store in my own database?**
Store the source platform slug together with the platform's own lot id — for example `encar/38112900`. That pair is stable, is what every detail, image and price-history endpoint is addressed by, and survives reindexing. Avoid depending on a provider's synthetic internal id unless they guarantee it is stable across reindexes.
**Why do auction listing images stop working after a few weeks?**
Source platforms expire media URLs once a lot closes, and many apply hotlink protection that checks the request referrer. Any product that displays historical listings needs images copied and re-hosted at ingest time rather than linked, or its galleries will decay as lots close.
**Do I need historical auction data, or is live inventory enough?**
Live inventory is enough to build a search product. It is not enough to answer whether a given lot is well priced, which requires comparable closed sales, nor to train any pricing model. If price intelligence is anywhere on your roadmap, confirm archive access before you choose a provider — retrofitting history you never collected is impossible.