contract 2026-08-19
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 — japanauction 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

Top offers already carry the market reference the verdict was made against:

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"}' \
  "$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.