contract 2026-09-03
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 search examples use cURL with jq, a server-side TypeScript runtime, Python with requests, or PHP with Guzzle. Set THECARAPI_KEY in that runtime. The recipes illustrate integration patterns and use your application’s existing state and helpers.

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_str, 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;
  total_pages: number | null;
  contract_version: string;
  request_id: string;
}

interface SearchCard {
  auction_id: number;
  auction_id_str: string;
  site_name: string;
  clean_make: string;
  clean_model: string;
  registration_year: number | null;
  mileage: number | null;
  /** Prices are JSON numbers, never strings. */
  public_price_eur?: number | null; // May be omitted by field permissions.
  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_str"], car.get("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/%s — €%s\n", $car['site_name'], $car['auction_id_str'], $car['public_price_eur'] ?? 'unavailable');
}

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
        # /api/search and /api/listVehicles have no offset cap. Catalog,
        # top-offers and classifieds listings stop at 5000 and a page too deep
        # answers 400. Read pagination from /api/contract.
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

  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  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.