contract 2026-09-03
Build

Recipes

Six end-to-end flows that cover most of what the API is for. Each is a complete sequence, not a fragment.

Set API=https://api.thecarapi.com and KEY=your_key first.

1. Build a filter sidebar in one request

Six dimensions, one round trip, one unit of quota:

bash
curl -sS -H "X-API-Key: $KEY" --compressed \
  "$API/api/facets?fields=brands,years,fuels,gearboxes,countries,sites"

Then narrow it as the user picks. Facets cross-filter: pass the filters already chosen and every remaining dimension re-counts against them, while each dimension still ignores its own filter so the user can change their mind.

bash
# After "BMW, under EUR 15,000": what fuels and years are left?
curl -sS -H "X-API-Key: $KEY" \
  "$API/api/facets?fields=fuels,years&brand=bmw&price_to=15000"

Models are per-brand and stay on their own route:

bash
curl -sS -H "X-API-Key: $KEY" "$API/api/models?brand=bmw&ordering=-count&limit=25"

Cache the sidebar. Facets are served max-age=600 with an ETag; store it and send If-None-Match and the refresh costs an empty 304.

2. Search, page it, and keep the totals honest

bash
curl -sS -H "X-API-Key: $KEY" --compressed \
  "$API/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=50&page=1"
  • Paging. Pick one spelling and stay with it — page+page_size, or offset+limit. Mixing them is a 400. Loop to total_pages; a filtered search's total and its rows now agree, so you will not run into short pages.
  • Counting is the expensive half. If you are streaming rather than showing a page count, send include_total=false and skip it entirely. If you want only the number, send count_only=true and get no rows.
  • Persist site_name + auction_id_str, never auction_id on its own — japan ids exceed 2^53.
bash
# "How many diesel BMWs are there?" — one cheap call, no rows.
curl -sS -H "X-API-Key: $KEY" \
  "$API/api/search?brand=bmw&fuel=Diesel&count_only=true" | jq .total

3. Open one car: detail, live bid, photos

One request gets specification, condition and gallery together:

bash
curl -sS -D headers.txt -H "X-API-Key: $KEY" --compressed \
  "$API/api/auction/openlane/11409652" -o car.json

Then branch on three things, in this order:

text
live_price          present  -> that IS the current bid; render it
live_price_pending  present  -> read once more after ~2s, then stop
neither                      -> the cycle price is the price; do not retry

is_blind: true      -> there will never be a price. Show estimated_value_eur,
                       labelled as the auction house's estimate.
details_pending: t  -> vehicle_details is not there yet. Re-read on the short
                       max-age the response carries; render the rest meanwhile.
vault_gallery.pending > 0 -> more photos are coming. Poll on max-age=10.

Photo URLs are paths, so join them to the base:

bash
jq -r '.auction.vault_gallery.images[] | .served_url // .remote_url' car.json \
  | sed "s|^/|$API/|"

You do not need /api/auction-images as well — vault_gallery is that same body embedded. Use the separate route only when you want the gallery alone.

4. Shop the discounts, then price the import

With the market_reference field grant, top offers carry the reference used for their scoring:

bash
curl -sS -H "X-API-Key: $KEY" \
  "$API/api/top-offers?site=openlane&min_savings_pct=20&sort=savings&limit=10"

Take one and cost it landed. Read is_margin off the detail response, and send the source so the fee model matches the one that priced the listing:

bash
curl -sS -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"price":9000,"site_name":"openlane","origin":"DE","destination":"BG","is_margin":false}' \
  "$API/api/calculator/calculate" | jq .breakdown.estimated_total

Compare that landed total with the retail market, not with the lot price:

bash
curl -sS -H "X-API-Key: $KEY" \
  "$API/api/cars-bg-market?brand=BMW&model=320d&year=2019&flex=1" \
  | jq '{n: .snapshot.listing_count, median: .snapshot.median_price_eur}'

Check listing_count before you trust the median. Market snapshots are not enabled on a new key by default — ask for them.

5. Track one car over time

Price movements for a lot you are watching:

bash
curl -sS -H "X-API-Key: $KEY" \
  "$API/api/auction/openlane/11409652/price-history" \
  | jq '.history[] | {observed_at, current_price, changed_fields}'

And its life before this listing, by VIN — this is what shows a car that has been through auction more than once:

bash
curl -sS -H "X-API-Key: $KEY" \
  "$API/api/vin/WBA8E9G50GNU12345/history" \
  | jq '.auctions[] | {site_name, last_seen_at, mileage, public_price_eur, archived}'

VIN history is not enabled on a new key by default — ask for it. event_type on price history is initial, baseline or change — there is no price_change value.

6. Mirror the whole feed, cheaply

To keep a local copy in sync rather than to answer a query:

bash
# Unfiltered = no depth limit. Walk it at the maximum page size.
curl -sS -H "X-API-Key: $KEY" -H "Accept-Encoding: gzip" --compressed \
  "$API/api/search?limit=100&offset=0&include_total=false"

Four things make this an order of magnitude cheaper:

  1. 1--compressed — roughly an eighth of the bytes.
  2. 2include_total=false — the count is the expensive half of a search.
  3. 3Store each page's ETag and send If-None-Match on the re-walk; unchanged pages answer 304 with no body.
  4. 4Do not expand every card. Hydrate detail only for rows whose last_changed_at moved, or that you actually display.

Search results are always cycle-priced, never live-priced. That is what makes them fast enough to page — see what stays on cycle prices.