Отговори и страниране
Обвивката на отговора, кеширане и условни заявки, компресия, синоними за страниране и заглавки за лимитите.
Обвивка на отговора
Успешните отговори използват success: true и ключ за данните, специфичен за маршрута. Основните отговори включват и полетата по-долу.
| Поле | Тип | Значение |
|---|---|---|
contract_version | string | Договор за схемата, в момента 2026-10-05. |
request_id | string | Идентификатор за корелация, връщан и в X-Request-ID. |
server_time | timestamp | Време на отговора в UTC ISO-8601. |
data_updated_at | timestamp | Време на данните, подадено от маршрута; по подразбиране е server_time, когато няма време на данните. Не е гарантирано време на обновяване от източника. |
GET /load-models отива още по-далеч и не връща дори флаг success: ключовете в отговора му са данни (имена на марки) и нищо друго не може да споделя това пространство. Ако пишете обща обвивка на клиента, която проверява body.success, обработете този маршрут отделно.
Можете да изпратите собствен X-Request-ID (до 80 знака) и той ще бъде върнат.
{
"success": true,
"results": [ /* … */ ],
"total": 18342,
"limit": 24,
"offset": 0,
"contract_version": "2026-10-05",
"request_id": "0f2c1b9e-4a77-4c31-9a0b-6d2f0a1c8e55",
"server_time": "2026-08-19T09:41:02Z",
"data_updated_at": "2026-08-19T06:00:00Z"
}Актуалност и липсващи стойности
| Сигнал | Значение | Поведение на клиента |
|---|---|---|
server_time / data_updated_at | Време на отговора и метаданни на маршрута; data_updated_at може по подразбиране да е server_time. | Не ги използвайте сами, за да твърдите, че източникът току-що е проверен. |
first_seen_at, last_seen_at, last_available_at, last_changed_at | Съхранени наблюдения и промени на тази обява, където са налични и разрешени. | Използвайте заедно с updated_at; обновяването на инвентара, обогатяването на детайлите и обработката на снимките са отделни. |
live_price.fetched_at | Кога е прочетено актуалното наддаване, в Unix секунди. | Показвайте възрастта му. Може да се използва повторно в рамките на TTL на актуалната цена. |
details_pending / live_price_pending | Обогатяването на детайлите или обновяването на наддаването още работи. | Показвайте известните данни на картата и използвайте ограничена отложена проверка; не правете плътен цикъл на запитвания. |
| Липсващо поле / null / празен масив | Права, липсващи доказателства от източника или незавършено обогатяване може да влияят на наличността. | Дръжте „неизвестно“ отделно от нула, false, „без произшествия“ и „без сервизна история“. |
Кеширане
Маршрутите за четене може да връщат X-Cache: HIT, MISS или STALE. Отговорите за фасети и каталог се кешират за около 600 секунди; плитките страници от търсенето — за около 300 секунди. Дълбоките страници от търсенето (offset > 5000) съзнателно не се кешират, а отговор с обновена актуална цена намалява своя max-age до TTL на актуалната цена.
Условни заявки: върнете ETag обратно
Пазете ETag от отговора и го поставяйте в If-None-Match при следващата заявка за същия адрес. Ако нищо не се е променило, получавате 304 Not Modified без тяло — продължете да използвате копието, което вече имате. Клиент, който проверява една и съща страница от търсенето на всеки няколко минути, заменя изтегляне от няколко мегабайта с празен отговор, докато резултатите не се променят.
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 получи ETag в договор 2026-08-19; маршрутите за фасети и каталог вече имаха. ETag в целия API са слаби (W/"…") по замисъл, така че един таг остава валиден независимо дали тялото е компресирано. Сравнявайте ги като непрозрачни низове и ги връщайте точно както са получени — не премахвайте префикса W/ или кавичките.
Свързвайте кешираните тела и ETag с пълния адрес на заявката и идентичността на API ключа. Никога не използвайте отговора на един клиент за друг ключ: правата за източници и полета може да се различават. Отговорът 304 няма JSON тяло, затова пазете кешираното тяло и не извиквайте response.json() върху него.
Поискайте компресия
Изпращайте Accept-Encoding: gzip и всеки отговор над 2 KB се връща компресиран — от порядъка на една осма от байтовете при пълна страница от търсене със 100 реда. Под този праг отговорите се изпращат без компресия, където допълнителните заглавки биха надвишили спестеното. Повечето HTTP клиенти договарят това и разкомпресират прозрачно; някои (особено PHP Guzzle в определени конфигурации) трябва да бъдат указани изрично.
// Guzzle: decode_content превръща "Accept-Encoding: gzip" в декодирано тяло.
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.thecarapi.com',
'headers' => ['X-API-Key' => getenv('THECARAPI_KEY')],
'decode_content' => 'gzip',
]);Страниране
| Параметър | Значение |
|---|---|
limit / page_size | Размер на страницата. Максимум 100; по-големите стойности се намаляват, а не се отказват. limit=0 или отрицателна стойност е 400. Не изпращайте и двата синонима. |
offset | Отместване на реда от нула. Не комбинирайте с page. |
page | Номер на страница от едно; съответства на offset = (page - 1) * limit. page=0 е 400. |
page / page_size (в отговора) | Същият прозорец в страници, върнат обратно, така че клиент, странирал с page, получава своя запис. И двете са null при отговор с count_only, който няма страници за описване. Носят ги /api/search, маршрутите на каталога и /api/listVehicles. /api/top-offers и /api/classifieds/listings връщат само total, limit, offset и total_pages — странирайте там с offset. |
total / total_pages | Null, когато общите бройки са изключени. |
max_page | Най-дълбоката достъпна страница според текущата политика за дълбочина. |
include_total | Задайте false, за да пропуснете преброяването. По-евтино при големи набори от резултати. |
Размерът на страницата по подразбиране е 100 за /api/search и 50 навсякъде другаде (каталог, /api/listVehicles, /api/top-offers, /api/classifieds/listings). limit е ограничен до 100 в целия API — по-големите стойности се намаляват, а не се отказват.
Политика за дълбочина
- Без ограничение на отместването.
/api/searchи/api/listVehiclesстранират до всякаква дълбочина, със или без филтри, така чеmax_pageв тези отговори е простоtotal_pages. Много дълбоко филтрирано търсене се обслужва от компактен индекс, където е възможно; където не е, подлежи на защитното изтичане на времето и може да върне503с молба да го стесните. Страниците след отместване 5000 също не се кешират, затова струват повече. - Ограничено до отместване 5000.
/api/catalog/*,/api/top-offersи/api/classifieds/listings. По-дълбока страница е400с посочен лимит; стеснете набора от резултати.
Актуалното ограничение се публикува като pagination.max_offset от GET /api/contract, заедно с pagination.max_limit. Четете тях, вместо да ги задавате ръчно.
Лимити на заявките
Управляваните ключове може да носят квоти с фиксиран прозорец за всеки от четири периода: минута, час, ден, месец. Прозорците са фиксирани, а не плъзгащи се — ключ с „60 на минута“ може да изпрати 60 заявки в 12:00:59 и още 60 в 12:01:00. Този изблик на границата е очакван.
Ключ може да бъде издаден и без квота и тогава не се изпраща нито една заглавка за лимитите — липсата им означава „неограничено“, а не „лимитът е достигнат“. Когато ключът има квоти, всяка заявка отчита прозореца с най-малко оставащ резерв, който е този, който ще ви засегне следващ. Всички зададени прозорци се проверяват, преди някой да бъде таксуван, така че заявка, отказана от дневния лимит, не изразходва и минута от минутната ви квота.
Надвишаването на квота връща 429 с X-RateLimit-Remaining: 0, посочва нарушения прозорец ({"success": false, "error": "Rate limit exceeded (1000 per hour)"}) и включва Retry-After — спазвайте го, вместо да изчаквате по собствен график. /api/facets струва една заявка от квотата ви, колкото и измерения да поискате, което е най-евтиният начин да изградите странична лента с филтри.
async function call(url, init = {}) {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch(url, init);
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
// 4xx, различно от 429, е грешка в заявката. Повторният опит не може да я поправи.
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 и 5xx. Никога при 400, 401, 403 или 404.