contract 2026-08-19
Reference

Code examples

Minimal clients in cURL, TypeScript, Python and PHP, plus recipes for pagination, conditional requests and detail pages.

There is no required SDK. Any HTTP client that can send headers and parse JSON will work. The examples below are complete — paste one, set THECARAPI_KEY, and it runs.

cURL

bash
export THECARAPI_KEY="your_key"

curl -sS --compressed \
  -H "X-API-Key: $THECARAPI_KEY" \
  "https://api.thecarapi.com/api/search?brand=bmw&fuel=Diesel&year_from=2018&sort=price_low&limit=24" \
  | jq '.results[] | {auction_id, site_name, clean_make, clean_model, public_price_eur}'

TypeScript

typescript
const BASE = "https://api.thecarapi.com";

interface SearchResponse {
  success: boolean;
  results: SearchCard[];
  total: number | null;
  limit: number;
  offset: number;
  max_page: number | null;
  random_seed: string | null;
  contract_version: string;
  request_id: string;
}

interface SearchCard {
  auction_id: number;
  site_name: string;
  clean_make: string;
  clean_model: string;
  registration_year: number;
  mileage: number;
  /** Prices are JSON numbers, never strings. */
  public_price_eur: number;
  thumbnail_url: string | null;
}

async function search(params: Record<string, string | number>): Promise<SearchResponse> {
  const url = `${BASE}/api/search?${new URLSearchParams(
    Object.entries(params).map(([k, v]) => [k, String(v)]),
  )}`;

  const res = await fetch(url, {
    headers: {
      "X-API-Key": process.env.THECARAPI_KEY!,
      "Accept-Encoding": "gzip",
    },
  });

  if (!res.ok) {
    throw new Error(`${res.status} — request ${res.headers.get("X-Request-ID")}`);
  }
  return res.json();
}

const page = await search({ brand: "bmw", fuel: "Diesel", limit: 24 });
console.log(page.results.length, "of", page.total);

Python

python
import os
import requests

BASE = "https://api.thecarapi.com"

session = requests.Session()
session.headers.update({"X-API-Key": os.environ["THECARAPI_KEY"]})
# requests negotiates gzip and decompresses transparently.

response = session.get(
    f"{BASE}/api/search",
    params={"brand": "bmw", "fuel": "Diesel", "year_from": 2018, "limit": 24},
    timeout=30,
)
response.raise_for_status()
payload = response.json()

for car in payload["results"]:
    print(car["site_name"], car["auction_id"], car["public_price_eur"])

PHP

php
<?php
$client = new GuzzleHttp\Client([
    'base_uri'       => 'https://api.thecarapi.com',
    'headers'        => ['X-API-Key' => getenv('THECARAPI_KEY')],
    'decode_content' => 'gzip',   // Guzzle needs this told to it explicitly
    'timeout'        => 30,
]);

$response = $client->get('/api/search', [
    'query' => ['brand' => 'bmw', 'fuel' => 'Diesel', 'limit' => 24],
]);

$payload = json_decode((string) $response->getBody(), true);
foreach ($payload['results'] as $car) {
    printf("%s/%d — €%s\n", $car['site_name'], $car['auction_id'], $car['public_price_eur']);
}

Recipe: page through a result set

python
def iter_results(session, **filters):
    """Yield every card, respecting the depth policy rather than guessing at it."""
    offset, limit = 0, 100
    while True:
        r = session.get(f"{BASE}/api/search",
                        params={**filters, "limit": limit, "offset": offset,
                                "include_total": "false"},
                        timeout=30)
        r.raise_for_status()
        rows = r.json()["results"]
        if not rows:
            return
        yield from rows
        offset += limit
        if offset >= 5000:      # public depth cap; read it from /api/contract
            return
include_total=false skips the count, which is the expensive half of a deep search.

Recipe: poll a search page without re-downloading it

javascript
let etag = null;
let cached = null;

async function poll(url) {
  const res = await fetch(url, {
    headers: {
      "X-API-Key": process.env.THECARAPI_KEY,
      ...(etag ? { "If-None-Match": etag } : {}),
    },
  });

  if (res.status === 304) return cached;   // nothing changed, no body transferred

  etag = res.headers.get("ETag");          // opaque — echo it back exactly
  cached = await res.json();
  return cached;
}

Recipe: a vehicle detail page in one request

javascript
const { auction } = await getJson(`/api/auction/${site}/${id}`);

const photos = auction.vault_gallery?.images ?? [];
const stillComing = auction.vault_gallery?.pending ?? 0;   // render placeholders

const price = auction.public_price_eur;                     // already live if it could be
const asOf = auction.live_price
  ? new Date(auction.live_price.fetched_at * 1000)          // seconds → ms
  : null;

// Only if the refresh missed the request budget, read exactly once more.
if (!auction.live_price && auction.live_price_pending) {
  setTimeout(() => refetch(site, id), 2000);
}
vault_gallery is the same body /api/auction-images/{site}/{id} returns, so the second request is unnecessary.