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.
# 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"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.
# 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 += 100Seed 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.
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
0means the odometer read zero.nullmeans the source did not say. Rendering both as "0 km" makes every unknown-mileage car look new.
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:
- 1If images fail, render cards with a placeholder. Do not fail the page.
- 2If facets fail, render search without filter counts. Users can still search.
- 3If price history fails, hide the chart. It is an enhancement, not the product.
- 4If 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.
curl -s "https://api.thecarapi.com/api/health"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=falseon 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, and building a marketplace covers the architecture these mechanics sit inside.
Frequently asked questions
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.
- API integration
- caching
- rate limits
- pagination
One API, seven auction sources
Normalized search, source-aware detail, CDN image galleries, price history and archive access — all included on every plan.