Для разработчиков · v1
Car2B Import API
Публичный API для компаний с собственной автоматизацией: ваша система сама создаёт, обновляет и снимает объявления в Car2B. Ключ выпускается в личном кабинете, объявления проходят обычную модерацию.
https://car2b.ru/api/v1/import Формат: JSON, UTF-8 Ошибки: application/problem+json Содержание
Как это работает
- Владелец или администратор компании создаёт ключ доступа в кабинете. Секрет показывается один раз.
- Ваша система меняет пару
client_id+ секрет на токен на 5 минут и повторяет обмен по мере истечения. - С токеном она вызывает
PUT /import/listings/{external_id}: создаёт или обновляет объявление по вашему идентификатору. Повтор запроса безопасен. - Фото Car2B скачивает сам по вашим URL (или принимает файлом), сжимает и отправляет объявление на модерацию.
- Статус и вердикт модерации ваша система читает через
GET /import/listings/{external_id}.
1. Ключ доступа
Откройте Профиль → Интеграции в приложении Car2B и нажмите «Создать ключ». Раздел доступен владельцу и администраторам верифицированной компании. Задайте название (например, «1С:Автосалон, прод») и при желании — список разрешённых IP-адресов или подсетей.
- После создания вы увидите
client_idи секрет. Секрет показывается один раз — сохраните его в настройках вашей системы. Дальше в кабинете виден только префикс ключа. - «Перевыпустить» создаёт новый секрет; старый действует ещё 24 часа, чтобы обновить настройки без простоя.
- «Отозвать» останавливает ключ: новые токены не выдаются, уже выданные доживают не больше 5 минут.
- На организацию — до 5 активных ключей. Ключ принадлежит компании, а не сотруднику: увольнение выпустившего ключ ничего не ломает.
2. Обмен на токен
Стандартный OAuth 2.0 client_credentials. Секрет передаётся только в этом запросе, в заголовке HTTP Basic
(base64 от строки client_id:секрет). Все остальные вызовы идут с полученным Bearer-токеном.
curl -X POST https://car2b.ru/api/v1/identity/oauth/token \
-u "c2b_7Q4KJX2P9LMN3RTV8WQZ5M2:cs_live_4nR8vK2pQ7xL9mW3tY6bH1jF5dS0aG2c" \
-d "grant_type=client_credentials" {
"access_token": "eyJhbGciOiJIUzI1NiJ9…",
"token_type": "Bearer",
"expires_in": 300,
"scope": "import:listings.write import:listings.read"
} - Токен живёт 300 секунд. Кэшируйте его и обновляйте за минуту до истечения.
- На
401 token_expiredобновите токен и повторите запрос один раз. refresh_tokenне выдаётся: у машины есть секрет, она просто обменивает его снова.- Ошибки обмена:
401 invalid_client(неизвестный ключ, отозван или неверный секрет),403 organization_not_verified,429при более чем 10 обменах в минуту.
3. Загрузка объявления
PUT /import/listings/{external_id} создаёт или полностью заменяет объявление.
external_id задаёте вы: до 64 символов (латиница, цифры, . _ : -, первый символ —
буква или цифра), уникален в рамках ключа — обычно это идентификатор машины в вашей учётной системе. Поля, которых нет в теле, считаются пустыми. Ответ 202: объявление
принято, фото и модерация идут асинхронно.
curl -X PUT https://car2b.ru/api/v1/import/listings/DLR-000123 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @listing.json listing.json:
{
"vin": "XTT316300P1234567",
"brand": "УАЗ",
"model": "Патриот",
"year": 2023,
"mileage": 0,
"condition": "new",
"availability": "in_stock",
"body_type": "suv",
"color": "white",
"engine_volume": 2.7,
"engine_power": 150,
"fuel_type": "petrol",
"transmission": "manual",
"drive_type": "awd",
"steering_wheel": "left",
"price": { "amount": 1990000, "currency": "RUB", "vat_included": true },
"price_car2b": 1890000,
"city": "Москва",
"description": "Новый автомобиль, в наличии.",
"quantity": 1,
"photos": [
{ "url": "https://cdn.dealer.ru/cars/123/1.jpg" },
{ "url": "https://cdn.dealer.ru/cars/123/2.jpg" },
{ "file_id": "6d1f2c8e-9b7a-4c1e-8f3d-2a5b6c7d8e9f" }
]
} Ответ:
HTTP/1.1 202 Accepted
{
"external_id": "DLR-000123",
"listing_id": "9f2c1a44-…",
"status": "pending_photos",
"created": true,
"missing_fields": [],
"resolved": {
"brand": { "title": "УАЗ", "confidence": 1.0 },
"model": { "title": "Патриот", "confidence": 0.97 }
},
"photos": [
{ "url": "https://cdn.dealer.ru/cars/123/1.jpg", "status": "pending" },
{ "url": "https://cdn.dealer.ru/cars/123/2.jpg", "status": "pending" },
{ "file_id": "6d1f2c8e-…", "status": "uploaded" }
],
"warnings": []
}
Если марка или модель распознаны неуверенно, придёт 422 unresolved_reference со списком кандидатов.
Уточните название или передайте стабильный идентификатор name из справочника:
GET /import/reference/brands и GET /import/reference/models?brand=<name>
отдают { "items": [{ "name", "title" }] }.
VIN сверяется с маркой. Объявление той же организации с тем же VIN обновляется, а не дублируется;
VIN из активного объявления другого продавца даёт 409 vin_conflict.
PUT перезапишет его правки. В кабинете такое
объявление помечено «Управляется по API», а кнопка «Отвязать от API» переводит его в ручной режим: дальнейшие
PUT с этим external_id получат 409 listing_detached.
Поля объявления
Актуальный список полей с типами, обязательностью и допустимыми значениями отдаёт
GET /import/schema — модель данных динамическая, документация её не дублирует. Обязательные поля не
блокируют приём: без них объявление сохраняется черновиком (draft) со списком missing_fields.
| Поле | Тип | Примечание | |
|---|---|---|---|
brand, model | строка | обязательно | название сопоставляется автоматически; точнее — name из справочника |
year | число | обязательно | 4 цифры |
price.amount | число | обязательно | рубли без копеек; vat_included — цена с НДС |
availability | enum | обязательно | значения из справочника (GET /import/schema); гарантированно принимается in_stock. Площадка пока работает только с авто в наличии: on_order и in_transit отклоняются с 422 и сообщением по полю — то же ограничение, что в кабинете |
vin | строка | рекомендуется | без VIN дедупликация только по external_id; WMI сверяется с маркой |
price_car2b | число | опционально | цена для Car2B, должна быть ниже розничной |
mileage, condition | число, enum | опционально | км; 0 или пусто — новая; new / used |
generation, body_type, color, interior_color, interior_material | enum / строка | опционально | значения — из /import/schema |
engine_volume, engine_power, fuel_type, transmission, drive_type, steering_wheel | число / enum | опционально | литры, л. с. |
city | строка | опционально | по умолчанию — город организации |
description | строка | опционально | до 4000 символов, проходит текстовую проверку |
quantity | число | опционально | N одинаковых новых машин — N объявлений, до 100 |
photos[] | {url} или {file_id} | опционально | порядок в массиве — порядок показа, до 30 |
Фото
Два способа доставки, один конвейер обработки: конвертация HEIC, сжатие до 1920 px по длинной стороне, дедупликация, загрузка в хранилище Car2B.
- URL — основной путь. Укажите
photos: [{ "url": "…" }], Car2B скачает файлы сам. ПовторныйPUTс теми же URL не перекачивает фото. Адрес должен быть публичным: http/https, порты 80 и 443, без приватных сетей. - Файл — если фото не опубликованы по URL. Загрузите каждое через
POST /import/photos(multipart, полеfile, до 20 МБ) и подставьте полученныйfile_id. - Base64 внутри JSON не принимается.
curl -X POST https://car2b.ru/api/v1/import/photos \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/photo.jpg"
{ "file_id": "6d1f2c8e-9b7a-4c1e-8f3d-2a5b6c7d8e9f" }
Статус каждого фото виден в ответе: pending uploaded failed с причиной — fetch_timeout, not_image, too_large,
forbidden_host, http_4xx, fetch_failed. Скачивание повторяется три раза с ростом интервала. Объявление без
единого удачного фото остаётся в pending_photos и на модерацию не идёт.
4. Статусы
GET /import/listings/{external_id} возвращает статус объявления, статус каждого фото, список
незаполненных обязательных полей, вердикт модерации с причиной, идентификатор объявления в Car2B и публичную ссылку.
curl https://car2b.ru/api/v1/import/listings/DLR-000123 \
-H "Authorization: Bearer $TOKEN" | Статус | Что значит | Что делать |
|---|---|---|
| draft | принято, не хватает обязательных полей | посмотреть missing_fields и прислать PUT |
| pending_photos | фото ещё скачиваются и обрабатываются | подождать, опрашивать не чаще раза в минуту |
| moderation | объявление у модератора | ничего |
| published | видно в каталоге, есть public_url | ничего |
| rejected | отклонено, причина в moderation.reason | исправить и прислать PUT |
| archived | снято по DELETE или удалено на стороне Car2B | PUT создаст новое объявление |
На модерацию объявление уходит автоматически, как только заполнены обязательные поля и загружено хотя бы одно фото.
Отдельного вызова «опубликовать» нет. Правки опубликованного объявления, меняющие название, описание, розничную цену или валюту, VIN, фото,
марку, модель или год, возвращают его на модерацию: status становится moderation,
объявление скрывается из каталога до одобрения. Изменение наличия, пробега и цены Car2B применяется сразу
и статус не меняет.
Если вебхук не настроен — опрашивайте статус не чаще раза в минуту, см. раздел «Вебхуки».
Список и снятие с публикации
GET /import/listings?cursor=&limit=— всё, что загружено этим ключом, с курсором. Нужен для сверки: что есть в Car2B, чего уже нет у вас.DELETE /import/listings/{external_id}— снять с публикации и архивировать (204). ПовторныйPUTс тем жеexternal_idсоздаст новое объявление. Временно убрать машину с витрины черезavailabilityпока нельзя — снимайте черезDELETE.
Пакетная загрузка
POST /import/listings:batch принимает до 100 объявлений за один вызов — тот же формат тела,
что у PUT, плюс external_id у каждого элемента. Ответ 202 с
run_id, числом принятых и списком отклонённых с ошибками по каждому. Принятые обрабатываются
так же, как при одиночном PUT: фото, статусы, правило модерации правок опубликованных
объявлений (см. раздел статусов), вебхуки.
- Ставьте заголовок
Idempotency-Key(до 128 символов, например идентификатор выгрузки из вашей системы): повтор с тем же ключом в течение 24 часов вернёт первый ответ с заголовкомIdempotency-Replayed: trueи не создаст второй прогон. - Строки проверяются сразу: невалидные возвращаются в
rejectedс ошибками, принятые применяются асинхронно. Ход обработки —GET /import/runs/{run_id}: статус прогона (queued,running,done), счётчики создано/обновлено/отклонено/ошибок и построчные исходы с курсором; ошибки строки — те же коды, что уPUT. - Отклонённые строки не блокируют остальные: исправьте их и пришлите отдельным вызовом. Тело пакета — до 5 МБ; суточная квота считает каждую принятую строку.
curl -X POST https://car2b.ru/api/v1/import/listings:batch \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 2026-09-24T10:00:00-stock-sync" \
-H "Content-Type: application/json" \
-d '{ "items": [ { "external_id": "DLR-000123", ... }, { "external_id": "DLR-000124", ... } ] }'
HTTP/1.1 202 Accepted
{
"run_id": "3b1c…",
"accepted": 98,
"rejected": [
{ "external_id": "DLR-000777",
"errors": [ { "field": "year", "code": "invalid_value", "message": "Год выпуска: 4 цифры" } ] }
]
} Частичное обновление
PATCH /import/listings/{external_id} меняет только переданные поля
(JSON merge-patch): чего нет в теле — не трогается, null очищает поле. Так удобно обновлять
цену и наличие по расписанию, не пересылая всё объявление.
- Наличие, пробег и цена Car2B применяются сразу и статус не меняют. Название, описание, розничная цена
или валюта, VIN, фото, марка, модель или год у опубликованного объявления возвращают его на модерацию
(
status: moderation, объявление скрыто из каталога до одобрения) — как и приPUT. - Поля, которых нет в теле, не меняются — это главное отличие от
PUT, который заменяет объявление целиком. Тип тела —application/merge-patch+json(принимается иapplication/json), ответ202с тем же видом объявления, что уGET. - Ответ — тот же вид объявления, что у
GET; ошибки — как уPUT.
curl -X PATCH https://car2b.ru/api/v1/import/listings/DLR-000123 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{ "price": { "amount": 1950000 }, "mileage": 15000, "description": null }' Вебхуки
Вместо опроса статуса Car2B может сам сообщать о переходах. Адрес и секрет подписи задаются в кабинете, в карточке ключа (Профиль → Интеграции → ключ): адрес — только https и публичный домен; секрет показывается один раз, перевыпуск делает старую подпись недействительной.
listing.status_changed— любой переход статуса объявления (см. таблицу статусов), вdata.reason— причина отклонения.photo.failed— фото не удалось скачать или обработать; вdata—external_id,listing_id,urlилиfile_idиreason.
POST https://crm.example.ru/car2b/webhook
Content-Type: application/json
X-Car2B-Event: listing.status_changed
X-Car2B-Delivery: 7f0d3c1a-5e2b-4a7c-9d61-0b8e4f2a1c33
X-Car2B-Signature: sha256=3f5a9c…e1
{
"id": "7f0d3c1a-5e2b-4a7c-9d61-0b8e4f2a1c33",
"type": "listing.status_changed",
"occurred_at": "2026-09-24T11:02:15Z",
"data": {
"external_id": "DLR-000123",
"listing_id": "9f2c1a44-…",
"status": "published",
"previous_status": "moderation",
"reason": null
}
} | Заголовок | Что в нём |
|---|---|
X-Car2B-Signature | sha256= + hex(HMAC-SHA256(секрет, сырое тело запроса)) |
X-Car2B-Delivery | идентификатор доставки; повторы одного события приходят с тем же значением — используйте для дедупликации |
X-Car2B-Event | тип события, дублирует type в теле |
Ответьте любым 2xx в течение 10 секунд — этого достаточно, обрабатывать событие можно после
ответа. Иначе доставка повторяется до 5 раз с интервалами 1, 5, 15, 60 и 300 секунд. После пяти
неудач подряд карточка ключа в кабинете показывает «вебхук не отвечает».
Проверка подписи — всегда по сырому телу, до разбора JSON, сравнение за постоянное время:
import hmac, hashlib
def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
# raw_body — тело запроса как пришло, до разбора JSON import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, signatureHeader) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return expected.length === signatureHeader.length
&& timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Журнал доставок виден в карточке ключа и доступен по API: GET /import/webhooks/deliveries?cursor=&limit=
(новые сверху, до 200 на страницу). У каждой записи — событие, попытка, state
(pending, delivered, failed), код ответа (0 — ответа не было)
и текст ошибки; поле webhook_failing_since в ответе появляется, когда последние пять доставок
подряд провалились.
Безопасность ключа
- Разрешённые IP. В карточке ключа можно задать до 20 адресов или подсетей (CIDR).
Тогда обмен ключа на токен и каждый вызов API с другого адреса отклоняются:
401 invalid_clientна обмене,403 ip_not_allowedна запросе. Пустой список — вызовы принимаются откуда угодно. - Отзыв. После отзыва ключа новые токены не выдаются, а уже выданный перестаёт
приниматься не позже чем через 30 секунд — с ответом
401 client_revoked. Обновить токен по отозванному ключу нельзя. - Секреты. Секрет ключа и секрет подписи вебхука показываются один раз и хранятся только у вас. Утёк — перевыпустите: секрет ключа живёт параллельно старому ещё 24 часа, секрет подписи меняется сразу.
Лимиты
| Что | Значение |
|---|---|
| Запросов в секунду на ключ | 10, всплеск до 50 |
| Объявлений в одном пакетном вызове | 100, тело до 5 МБ |
| Idempotency-Key пакетного вызова | помнится 24 часа |
| Загрузок (PUT) в сутки на организацию | 5 000 |
| Обменов ключа на токен в минуту | 10 |
| Тело запроса | 1 МБ |
| Фото на объявление | 30 |
| Размер одного фото | 20 МБ, до 50 Мпикс |
| Скачивание фото по URL | 30 с, до 3 редиректов, только http/https на портах 80 и 443 |
| Активных ключей на организацию | 5 |
При превышении лимита запросов приходит 429 с заголовком Retry-After — подождите указанное число секунд.
Ошибки
Ошибки приходят в формате application/problem+json. В errors[] — поле, код, сообщение
на русском и, где уместно, кандидаты для исправления. Сообщения не содержат внутренних идентификаторов;
в 409 vin_conflict владелец VIN не раскрывается.
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.car2b.ru/errors/validation",
"title": "Объявление не прошло проверку",
"status": 422,
"instance": "/api/v1/import/listings/DLR-000123",
"errors": [
{ "field": "model", "code": "unresolved_reference",
"message": "Модель «Патриот Спорт» не найдена у марки УАЗ",
"candidates": [ { "name": "patriot", "title": "Патриот", "confidence": 0.71 } ] }
]
} | HTTP | code | Когда |
|---|---|---|
| 400 | malformed, invalid_value | невалидный JSON, неизвестное поле, неверный тип; неверный параметр запроса |
| 401 | token_invalid, token_expired, client_revoked | обновите токен через /oauth/token и повторите запрос один раз; client_revoked — ключ отозван, обновлять нечего |
| 402 | slots_exhausted | тариф организации не даёт свободных слотов под объявления |
| 403 | scope_missing, organization_blocked, organization_unverified, ip_not_allowed | ключ не имеет права на действие |
| 404 | listing_not_found | нет такого external_id у этого ключа |
| 409 | vin_conflict, listing_detached | VIN уже в активном объявлении другого продавца; объявление отвязано от API в кабинете |
| 413 | payload_too_large | тело больше 1 МБ или фото больше 20 МБ |
| 422 | unresolved_reference, vin_brand_mismatch, price_car2b_above_retail, invalid_enum, invalid_value, required_field_missing, unknown_file_id, not_image, rejected_by_catalog | поля разобраны, но не проходят правила; у unresolved_reference есть candidates |
| 429 | rate_limited, daily_quota_exceeded | превышен лимит, в ответе заголовок Retry-After |
Спецификация
Точное описание ручек, тел запросов и ответов — в OpenAPI-спецификации: интерактивная версия и файл YAML для генерации клиента.
Как проверить интеграцию
Отдельной песочницы нет — проверяйте на рабочем контуре. Выпустите в кабинете отдельный ключ для тестов
(например, с названием «Тест»). Объявления, загруженные по ключу, попадают на модерацию и не публикуются
до одобрения, поэтому тестовые загрузки безопасны. Давайте им явные external_id
(например, TEST-001) и снимайте через DELETE после проверки. Отозвать
тестовый ключ можно в любой момент.
Вопросы по интеграции: контакты Car2B.