Responses & pagination
The response envelope, caching and conditional requests, compression, pagination aliases, and rate-limit headers.
Response envelope
Successful responses use success: true and a route-specific payload key. Primary responses also include the fields below.
| Field | Type | Meaning |
|---|---|---|
contract_version | string | Schema contract, currently 2026-08-19. |
request_id | string | Correlation id, also returned in X-Request-ID. |
server_time | timestamp | UTC ISO-8601 response time. |
data_updated_at | timestamp | Time the underlying data was last refreshed. |
{
"success": true,
"results": [ /* … */ ],
"total": 18342,
"limit": 24,
"offset": 0,
"contract_version": "2026-08-19",
"request_id": "0f2c1b9e-4a77-4c31-9a0b-6d2f0a1c8e55",
"server_time": "2026-08-19T09:41:02Z",
"data_updated_at": "2026-08-19T06:00:00Z"
}Caching
Read routes can return X-Cache: HIT, MISS, or STALE. Facet and catalog responses cache for about 600 seconds; shallow search pages cache for about 300 seconds. Deep search pages (offset > 5000) are intentionally not cached, and a response carrying a refreshed live price drops its max-age to the live-price TTL.
Conditional requests: send the ETag back
Store the ETag from a response and put it in If-None-Match on the next request for the same URL. If nothing changed you get 304 Not Modified with no body — keep using the copy you already hold. A client polling the same search page every few minutes replaces a multi-megabyte download with an empty response for as long as the results hold still.
GET /api/search?site=openlane&limit=100 HTTP/1.1
X-API-Key: your_key
If-None-Match: W/"6f1c0a9d8e..."
HTTP/1.1 304 Not Modified
ETag: W/"6f1c0a9d8e..."/api/search gained an ETag in contract 2026-08-19; the facet and catalog routes already had one. ETags are weak (W/"…") across the API by design, so one tag stays valid whether or not the body came back compressed. Compare them as opaque strings and echo them back exactly as received — do not strip the W/ prefix or the quotes.
Ask for compression
Send Accept-Encoding: gzip and any response over 2 KB comes back gzipped — on the order of an eighth of the bytes on a full 100-row search page. Below that threshold responses are sent uncompressed, where the header overhead would outweigh the saving. Most HTTP clients negotiate this and decompress transparently; a few (notably PHP Guzzle in certain configurations) have to be told to.
// Guzzle: decode_content is what turns "Accept-Encoding: gzip" into a decoded body.
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.thecarapi.com',
'headers' => ['X-API-Key' => getenv('THECARAPI_KEY')],
'decode_content' => 'gzip',
]);Pagination
| Parameter | Meaning |
|---|---|
limit / page_size | Page size, maximum 100. Do not send both aliases. |
offset | Zero-based row offset. Do not combine with page. |
page | One-based page number. |
total / total_pages | Null when totals are suppressed. |
max_page | Deepest reachable page under the current depth policy. |
include_total | Set false to skip counting. Cheaper on large result sets. |
Public keys are capped at limit=100 and offset=5000. Invalid alias combinations return 400. Read the real caps from pagination on /api/contract rather than hardcoding them.
Rate limits
Quotas use fixed minute, hour, day or month windows. Inspect X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 response also includes Retry-After — honour it rather than backing off on a schedule of your own.
async function call(url, init = {}) {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch(url, init);
if (res.status === 429 || res.status === 503) {
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
// 4xx other than 429 is a bug in the request. Retrying cannot fix it.
if (!res.ok && res.status < 500) {
throw new Error(`${res.status} ${await res.text()} (request ${res.headers.get("X-Request-ID")})`);
}
return res;
}
throw new Error("giving up after 4 attempts");
}429, 503 and 5xx. Never retry 400, 401, 403 or 404.