Car2B

Для разработчиков · v1

Car2B Import API

Публичный API для компаний с собственной автоматизацией: ваша система сама создаёт, обновляет и снимает объявления в Car2B. Ключ выпускается в личном кабинете, объявления проходят обычную модерацию.

Базовый адрес: https://car2b.ru/api/v1/import Формат: JSON, UTF-8 Ошибки: application/problem+json
Содержание

Как это работает

  1. Владелец или администратор компании создаёт ключ доступа в кабинете. Секрет показывается один раз.
  2. Ваша система меняет пару client_id + секрет на токен на 5 минут и повторяет обмен по мере истечения.
  3. С токеном она вызывает PUT /import/listings/{external_id}: создаёт или обновляет объявление по вашему идентификатору. Повтор запроса безопасен.
  4. Фото Car2B скачивает сам по вашим URL (или принимает файлом), сжимает и отправляет объявление на модерацию.
  5. Статус и вердикт модерации ваша система читает через GET /import/listings/{external_id}.
Объявления, загруженные по ключу, создаются от имени владельца компании и подчиняются тем же правилам, что и созданные вручную: верификация продавца, слоты тарифа, реестр VIN, модерация.

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 — цена с НДС
availabilityenumобязательнозначения из справочника (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_materialenum / строкаопциональнозначения — из /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-Signaturesha256= + 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 Мпикс
Скачивание фото по URL30 с, до 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 } ] }
  ]
}
HTTPcodeКогда
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.