# Отправляйте свой прайс в ADAM

Один HTTP-запрос заменяет таблицу. Ваша система отправляет выгрузку, а мы строка за строкой говорим, что прочитали и чего не смогли.

Проверить payload, ничего не создавая, можно в Настройки → Интеграции → Песочница.

- OpenAPI 3.1: https://adam.ua/ru/docs/api/openapi.json
- HTML: https://adam.ua/ru/docs/api

## Как получить ключ

Настройки → Интеграции → Подключить. Секрет показывается один раз при создании и хранится только как хеш — держите его там, где его читает ваша ERP. Отправляйте как `Authorization: Bearer …`.

```bash
curl -X POST https://ingest.adam.ua/v1/products \
  -H "Authorization: Bearer adam_live_…" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"sku":"KRB-46","name":"Карбамід 46% гранульований","brand":"Sumykhimprom","category":"Добрива","unit":"кг","price":12.5,"stock":1000,"warehouse":"Київський склад","moq":100,"updated_at":"2026-08-24T14:02:31Z"}]}'
```

## Позиция, поле за полем — `POST /v1/products`

| Поле | Тип | | Правило |
| --- | --- | --- | --- |
| `name` | string · ≤300 | **обязательное** | Название товара в вашей системе. Обрезается, не может быть пустым. |
| `brand` | string · ≤120 | **обязательное** | Распознаётся матчером внутри категории. |
| `category` | string · ≤120 | **обязательное** | В человеческом виде. Распознаётся по списку ниже. |
| `unit` | string · ≤40 | **обязательное** | В человеческом виде: «шт», «шт.», «кг», «т», «канистра» — все распознаются. |
| `price` | number | **обязательное** | Больше 0. Число, не строка; десятичный разделитель — «.». |
| `stock` | number | **обязательное** | 0 или больше. Ноль означает «нет в наличии», а не «неизвестно». |
| `sku` | string · ≤100 | необязательное | Ваш артикул. По схеме необязателен, но без него последующий PATCH не имеет по чему искать позицию. |
| `warehouse` | string · ≤200 | необязательное | Сверяется с вашими складами — сначала точное название или город, затем единственный склад, название которого содержит это значение. Если не совпало или значения нет вовсе, строка ляжет на ваш склад по умолчанию: единственный ваш склад либо тот, который вы отметили основным. Два кандидата — единственный случай, когда мы не угадываем: склад не проставляется, возвращается `warehouse_ambiguous`. |
| `moq` | number | необязательное | Больше 0. По умолчанию 1. |
| `description` | string · ≤500 | необязательное | Свободный текст. Используется, когда строка становится заявкой на новый товар каталога. |
| `updated_at` | rfc3339 | необязательное | Когда позиция последний раз менялась в вашей системе. Даёт правило порядка для PATCH. |

Поле, которое мы не читаем, ломает весь запрос, а не отбрасывается тихо. Тихое отбрасывание — это то, как вы начинаете верить, что задали валюту, которой не задавали.

## Ответ

202, потому что строки приняты в обработку, а сопоставление и проверка ещё впереди. Отклонения называют ваш собственный индекс позиции, поэтому выгрузка, которую вы правите поэтапно, сходится.

```json
{
  "request_id": "8f14e45f-ceea-467a-9c4c-1b0f0e5c9e2a",
  "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "accepted": 1198,
  "rejected": [
    {
      "index": 42,
      "sku": "XYZ-1",
      "reason": "category_unresolved",
      "detail": "«Хімія» matches no category"
    },
    {
      "index": 87,
      "sku": "KRB-99",
      "reason": "required_field_missing",
      "field": "brand"
    }
  ],
  "rejected_total": 2
}
```

## Коды причин — `POST /v1/products`

| Код | Область | Значение |
| --- | --- | --- |
| `invalid_envelope` | весь запрос | Тело запроса — не объект с массивом items. |
| `unknown_field` | весь запрос | Поле, которое мы не читаем. Возвращаем ошибку, а не молча отбрасываем — чтобы вы не считали, что задали то, чего не задали. |
| `idempotency_key_reused` | весь запрос | Этот Idempotency-Key уже использован для запроса с другим телом. Возьмите новый ключ — сохранённый результат принадлежит предыдущему запросу, а не этому. |
| `no_items` | весь запрос | items пуст. |
| `too_many_items` | весь запрос | Позиций больше лимита на запрос. Отправьте меньшими партиями. |
| `required_field_missing` | одна строка | Содержит `field` — обязательное поле, которого нет или оно пустое. На PATCH /v1/offers это всегда sku. |
| `field_too_long` | одна строка | Содержит `field`. Граница для каждого поля — в таблице выше. |
| `price_invalid` | одна строка | Не число или не больше 0. |
| `stock_invalid` | одна строка | Не число или отрицательное. Ноль допустим. |
| `moq_invalid` | одна строка | Не число или не больше 0. |
| `sku_duplicate_in_payload` | одна строка | Тот же артикул уже есть раньше в этом запросе. Побеждает первое вхождение. |
| `updated_at_invalid` | одна строка | Не RFC 3339, например 2026-08-24T14:02:31Z. |
| `review_backlog` | весь запрос | Ваши непроверенные строки достигли лимита. Завершите проверку, прежде чем отправлять ещё. |
| `category_unresolved` | одна строка | Значение не совпадает ни со slug, ни с псевдонимом. |
| `unit_unresolved` | одна строка | Значение не совпадает ни с одной единицей. |
| `wrong_endpoint` | одна строка | Вы отправили `discontinued` сюда. Снятие с продажи и возврат — это PATCH /v1/offers. |

Закрытый набор — ваша интеграция может на них ветвиться. Один код на отклонённую строку: строка, не прошедшая несколько проверок, сообщает первую по порядку выше.

## Обновление цены и остатка — `PATCH /v1/offers`

Когда позиция связана, `PATCH /v1/offers` меняет её цену и остаток без проверки и без участия человека. Вы отправляете тот же артикул, что уже отправляли в выгрузку товаров, а мы меняем предложение за ним. Это тот вызов, который ваша ERP делает по расписанию.

```bash
curl -X PATCH https://ingest.adam.ua/v1/offers \
  -H "Authorization: Bearer adam_live_…" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"sku":"KRB-46","price":12.9,"stock":840,"updated_at":"2026-08-25T09:00:00Z"},{"sku":"KAS-32","stock":0},{"sku":"OLD-01","discontinued":true}]}'
```

| Поле | Тип | | Правило |
| --- | --- | --- | --- |
| `sku` | string · ≤100 | **обязательное** | Ваш артикул, тот же, что отправляли в POST /v1/products. Единственный способ назвать позицию. |
| `price` | number | необязательное | Больше 0. Не отправляйте — цена останется как есть. |
| `stock` | number | необязательное | 0 или больше. Ноль ставит «нет в наличии» и оставляет предложение в каталоге. |
| `moq` | number | необязательное | Больше 0. Минимальная партия; единица измерения здесь не меняется. |
| `discontinued` | boolean | необязательное | true снимает предложение с продажи, false возвращает. Снятие всегда проходит; возврат может быть отклонён. |
| `updated_at` | rfc3339 | необязательное | Когда позиция последний раз менялась в вашей системе. Отправьте — и запоздавший повтор не перезапишет более новую цену; не отправите — победит последний пришедший запрос. То же время и те же значения — это no-op; то же время с другими значениями — equal_timestamp_conflict. |

Позиция требует `sku` и хотя бы одного из `price`, `stock`, `moq`, `discontinued`. Всё, что вы не отправили, остаётся как есть — поэтому проход только по остаткам ни разу не тронет цену. Один `sku` отклоняется, а не считается применённым.

Отправляйте `updated_at` — и повторы перестают быть опасными: позиция, чьё время старее последнего применённого, ничего не меняет и возвращается как `stale_update`. Не отправляйте — победит последний пришедший запрос: для одного планового задания это нормально, для двух — нет.

`unchanged` — это позиции, у которых предложение уже держало отправленные значения. Ничего не записано и ничего не сломано: ночная выгрузка, отправляющая всё, получает `applied` за изменившееся и `unchanged` за остальное, а не тысячи ошибок. Тот же `updated_at` с теми же значениями попадает сюда же; с другими значениями это `equal_timestamp_conflict`, потому что одинаковое время не может сказать, какая версия актуальна.

Снятие с продажи и возврат не симметричны, и лучше знать почему, чем выяснить это на практике. `discontinued: true` проходит всегда — убрать собственное предложение не требует ничьего разрешения. `discontinued: false` возвращает его только если предложение полное и ваш аккаунт вправе публиковать; иначе строка возвращается как `relist_blocked` с причиной в `detail`.

`not_published` — количество применённых строк на предложениях, которых фермер не видит: черновик, приостановленное предложение. Цена действительно изменилась, но на неё никто не смотрит. Патч никогда не публикует за вас, поэтому этот показатель — то, как вы об этом узнаёте, а не гадаете, почему ничего не сдвинулось.

```json
{
  "request_id": "1c9d4b7e-3f60-4a21-8de5-7a2b6c0f9e11",
  "applied": 1198,
  "unchanged": 613,
  "not_published": 42,
  "rejected": [
    {
      "index": 17,
      "sku": "ZIR-20",
      "reason": "sku_ambiguous",
      "detail": "7 of your offers carry this code: Зірочка Z-20, Зірочка Z-22, …"
    },
    {
      "index": 63,
      "sku": "KRB-46",
      "reason": "stale_update",
      "detail": "last applied 2026-08-25T09:00:00+00"
    }
  ],
  "rejected_total": 2
}
```

### Коды причин — `PATCH /v1/offers`

| Код | Область | Значение |
| --- | --- | --- |
| `no_change_requested` | одна строка | Только sku, без изменяемых полей. Один timestamp — не изменение: отправьте price, stock, moq или discontinued. |
| `discontinued_invalid` | одна строка | Присутствует, но не булево. `"yes"` — это не true. |
| `sku_unknown` | одна строка | Для этого кода нет активной связи. Отправьте позицию через POST /v1/products один раз — после подтверждения PATCH работает всегда. |
| `sku_pending_catalog` | одна строка | Вы уже отправляли этот код, и позиция ждёт нашу каталожную команду. Не отправляйте повторно — PATCH начнёт работать с этим кодом сразу после создания товара. |
| `sku_ambiguous` | одна строка | Этот код есть у нескольких ваших предложений, поэтому неясно, какую цену менять. `detail` перечисляет товары. Дайте им разные коды в своей системе. |
| `offer_missing` | одна строка | Связь есть, но её предложение исчезло. Отправьте позицию через POST /v1/products ещё раз. |
| `stale_update` | одна строка | Ваш updated_at старее последнего применённого, поэтому ничего не изменилось. `detail` содержит наш timestamp. |
| `equal_timestamp_conflict` | одна строка | Ваш updated_at совпадает с уже применённым, но значения другие — мы не можем определить, какое актуально, и отказываем, а не угадываем. Сдвиньте updated_at. Если значения совпадают, позиция идёт в `unchanged` и ошибки нет. |
| `relist_blocked` | одна строка | `discontinued: false` отклонён проверкой публикации. Снятие с продажи проходит всегда; возврат требует полного предложения и права публиковать. `detail` содержит причину. |
| `price_tiered` | одна строка | У этого предложения есть ступени цены по количеству, и одна фиксированная цена их не заменяет. Уберите ступени в кабинете — дальше позицией управляет API. |

Только коды, которые возвращает лишь этот эндпоинт. Все коды уровня запроса и все проверки значений из таблицы выше действуют и здесь, а `field` называет `sku` там, где выгрузка товаров назвала бы десяток полей.

## Автообновление — мы забираем выгрузку сами

Тот же контракт, что в `PATCH /v1/offers` — та же структура запроса, те же поля позиции, те же коды причин, тот же результат. Разница только в направлении: не ваша система обращается к нам, а мы раз в сутки обращаемся к URL на вашей стороне. Больше в конвейере не меняется ничего. Настраивается в Настройки → Интеграции.

Создавать товары оно не может. Артикул, которого мы не знаем, отклоняется как `sku_unknown`. Новые позиции так же идут через `POST /v1/products` и его очередь проверки; автообновление меняет цену и остаток на уже связанных позициях — и больше ничего.

Тело — то же, что вы уже отправляете в `PATCH /v1/offers`: массив `items`, объекты которого несут `sku` плюс `price`, `stock`, `moq`, `discontinued` и `updated_at`. Хотя бы одно из `price`, `stock`, `moq`, `discontinued` на позицию, иначе позиция отклоняется как `no_change_requested`. Нераспознанное поле отклоняет весь запрос как `unknown_field` — сознательно, а не тихим отбрасыванием. Максимум 5000 позиций (`too_many_items`), а одна партия свыше 16 MiB JSON — это `payload_too_large`.

Раз в сутки. Вы выбираете час, от 00 до 23, и часовой пояс, в котором он считается — поэтому 03:00 значит 03:00 там, где работает ваша система, а переход на летнее время и обратно мы учитываем сами. Следующий запуск показан на карточке. Выбирайте час, к которому ваша выгрузка уже записана, а не час, когда она начинается.

Аутентификация необязательна: либо ничего, либо один заголовок `Authorization: Bearer <token>`, который вы задаёте в Настройки → Интеграции. Токен хранится в зашифрованном виде, отправляется на ваш сервер при каждом обращении и никогда не попадает ни в лог, ни в аргументы процесса. 401 или 403 от вас — это `feed_http_error` с этим статусом, а не наш код аутентификации.

### Требования к вашему URL

Только `https://` — обычный HTTP отклоняется до того, как будет установлено соединение. Порт 443 или без порта вовсе: мы закрепляем адрес, в который разрешилось ваше доменное имя, через `curl --resolve host:443:<address>`, и это закрепление действует на пару (хост, порт) — поэтому URL на другом порте ушёл бы в обычный DNS в момент обращения и полностью отменил бы закрепление. Указывайте доменное имя, а не адрес — никаких IP-литералов и никакой их шестнадцатеричной, восьмеричной или целочисленной формы (`https://0x7f000001/` отклоняется), потому что клиент считает их адресом и не разрешает их вовсе.

Действующий TLS-сертификат, соответствующий этому доменному имени. Мы проверяем субъект сертификата, а не только его цепочку и срок действия — сертификат, действительный для другого имени, будет отклонён. И никаких перенаправлений: мы отправляем `--max-redirs 0`, поэтому 3xx возвращается вам как `feed_http_error` с его статусом. Отдавайте файл по тому URL, который вы нам дали.

Обычный `GET` — мы не отправляем ни тела, ни своих параметров запроса. Ответ должен быть корректным UTF-8; всё остальное — это `bad_encoding`, а Windows-1251 — типичная ошибка выгрузок из украинских и российских учётных систем. Предел — 16 MiB: выше него загрузка останавливается и вы получаете `too_large`. На ответ у вас 20 секунд; на разрешение вашего доменного имени перед этим отведены отдельные шесть секунд. Не кешируйте выгрузку надолго за CDN — мы забираем её раз в сутки, поэтому `Cache-Control` с max-age на часы или дни означает, что мы читаем вчерашний файл, а ваши цены выглядят застывшими. Ставьте короткий max-age или не ставьте его вовсе.

Когда вы сохраняете выгрузку, мы разрешаем ваше доменное имя и записываем адрес. При каждом следующем обращении мы разрешаем его снова и соединяемся только с тем адресом, который ваш собственный DNS публикует для этого имени сейчас, отдавая предпочтение записанному, пока он среди них. Два следствия, которые стоит назвать: переезд хостинга не требует от вас никаких действий, если DNS обновлён, и мы никогда не соединимся с адресом, который ваш DNS сейчас не называет. Если все адреса, которые он публикует, приватные, loopback, carrier-NAT, link-local или из зарезервированного пространства, запуск останавливается с `host_not_routable`; если он не публикует ни одного — `pin_unresolvable`.

### Когда запуск не удался

Каждый запуск оставляет строку в журнале запросов на экране интеграций, помеченную как пришедшую из автообновления, с подсчётами и отклонениями. Ошибка, с которой вы можете что-то сделать, дополнительно отправляет вам уведомление в приложении.

Сбои на нашей стороне — потерянная аренда запуска, наша база данных — сознательно не отправляют уведомление и помечены как наши, потому что это не то, что вы можете исправить, а следующий запланированный запуск повторяет попытку сам.

| Код | Область | Значение |
| --- | --- | --- |
| `bad_url` | весь запрос | Сохранённый URL больше не действителен. Введите его заново. |
| `bad_pin` | весь запрос | Не удалось прочитать сохранённый адрес. Проверьте его ещё раз и сохраните, чтобы заменить. |
| `pin_unresolvable` | весь запрос | Ваше доменное имя сейчас вообще не разрешается. Проверьте свой DNS, затем проверьте адрес ещё раз. |
| `host_not_routable` | весь запрос | Ваше доменное имя разрешается, но все адреса, которые оно сейчас публикует, — те, к которым мы не обращаемся: приватные, loopback, carrier-NAT, link-local или зарезервированные. Направьте доменное имя на публичный адрес. |
| `bad_auth` | весь запрос | Сохранённый токен не удалось использовать. Введите его заново и сохраните. |
| `too_large` | весь запрос | Ответ больше, чем мы принимаем. Разделите его или уберите ненужные поля. |
| `timeout` | весь запрос | Ваш сервер не ответил за отведённое время. |
| `fetch_failed` | весь запрос | Не удалось соединиться с вашим сервером. |
| `feed_http_error` | весь запрос | Ваш сервер ответил HTTP-ошибкой. Код 401 или 403 означает, что он не принял наш токен. |
| `bad_encoding` | весь запрос | Ответ не является корректным UTF-8. Отдавайте файл в UTF-8. |
| `malformed_json` | весь запрос | Ответ не является корректным JSON. |
| `payload_too_large` | весь запрос | Одна партия вашей выгрузки была слишком большой, чтобы мы могли её применить. Уберите ненужные поля или разделите файл. |
| `feed_not_found` | весь запрос | Настройки пропали, пока запуск ещё выполнялся. Сохраните их заново; если это повторяется, обратитесь в поддержку. |
| `lease_expired` | весь запрос | Мы начали запуск и вынуждены были прервать его до завершения. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова. |
| `lease_lost` | весь запрос | Это же задание параллельно подхватил другой запуск, поэтому мы откатили изменения и ничего не применили. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова. |
| `internal_error` | весь запрос | Что-то сломалось на нашей стороне при применении данных. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова. |
| `lock_timeout` | весь запрос | Что-то сломалось на нашей стороне при применении данных. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова. |
| `statement_timeout` | весь запрос | Что-то сломалось на нашей стороне при применении данных. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова. |

Коды, которые даёт только автообновление — они описывают сам запрос, до того как прочитана хоть одна позиция. Все коды из таблиц `PATCH /v1/offers` действуют для содержимого без изменений, потому что это то же содержимое. Последние три — наши, не ваши: следующий запланированный запуск повторит сам, и уведомления о них вы не получите.

## Категории, которые мы распознаём

Читается из каталога живьём, поэтому этот список не может разойтись с тем, что принимает резолвер. Регистр и конечная пунктуация не важны.

- `seeds` — Семена: Hybrids · Seed · seeds · Sowing material · Гибриды · Гібриди · Кукурудза · Кукуруза · Насіннєвий матеріал · Насіння · Насіння кукурудзи · Насіння рапсу · Насіння сої · Насіння соняшника · Посевной материал · Посівний матеріал · Пшеница · Пшениця · Рапс · Семена · Семена кукурузы · Семена подсолнечника · Семена подсолнуха · Семена рапса · Семена сои · Семенной материал · Соняшник · Соя · Ячмень · Ячмінь
- `fertilizers` — Удобрения: Fertiliser · Fertilizer · fertilizers · Growth stimulants · Micro fertilizers · Агрохимия · Агрохімія · Добрива · Микроудобрения · Микроудобрения и стимуляторы роста · Минеральные удобрения · Мікродобрива · Мікродобрива та стимулятори росту · Мінеральні добрива · Органические удобрения · Органічні добрива · Стимулятори росту · Стимуляторы роста · Удобрения
- `pesticides` — Средства защиты растений: Agrochemicals · CPP · Crop Protection · Fungicide · Fungicides · Herbicide · Herbicides · Insecticide · Insecticides · Pesticide · pesticides · Plant protection · Агрохимикаты · Агрохімікати · Адъюванты · Адьюванти · Акарициди · Акарициды · Гербицид · Гербициды · Гербіцид · Гербіциди · Десиканти · Десиканты · Засоби захисту · Засоби захисту рослин · ЗЗР · Инсектицид · Инсектициды · Інсектицид · Інсектициди · Пестициди · Пестициди та агрохімікати · Пестициды · Пестициды и агрохимикаты · Прилипатели · Прилипачі · Прилипачі (пав) · Протравители · Протруйник · Протруйники · Родентициди · Родентициды · СЗР · Средства защиты · Средства защиты растений · Фунгицид · Фунгициды · Фунгіцид · Фунгіциди
- `fuel-lubricants` — Горюче-смазочные материалы: Diesel · Fuel · fuel-lubricants · Grease · Lubricants · Oils · Антифриз · Бензин · Горюче-смазочные материалы · ГСМ · Дизельне пальне · Дизельное топливо · ДТ · Масла · Мастила · Пальне · Пально-мастильні матеріали · ПММ · Смазки · Топливо
- `machinery` — Техника: Combines · Equipment · Implements · machinery · Tractors · Комбайни · Комбайны · Обладнання · Оборудование · Прицепы · Причепи · Сельхозтехника · Сільгосптехніка · Техника · Техніка · Трактори · Тракторы
- `spare-parts` — Запчасти: Bearings · Belts · Filters · Parts · spare-parts · Детали · Деталі · Запчасти · Запчастини · Ножи · Ножі · Підшипники · Подшипники · Ремені · Ремни · Фильтры · Фільтри
- `tires-wheels` — Шины и диски: Rims · Tires · tires-wheels · Tyres · Wheels · Диски · Колеса · Колёса · Покришки · Покрышки · Шини · Шини та диски · Шины · Шины и диски
- `supplies` — Расходные материалы: supplies · Витратні матеріали · Расходные материалы · Спецодежда · Спецодяг · Тара · Упаковка
- `services` — Услуги: services · Послуги · Сервис · Сервіс · Услуги
- `livestock` — Животноводство: livestock · Ветпрепарати · Ветпрепараты · Животноводство · Корма · Корми · Тваринництво
- `energy-systems` — Энергосистемы: energy-systems · Генератори · Генераторы · Енергосистеми · Солнечные панели · Сонячні панелі · Энергосистемы

## Единицы, которые мы распознаём

- `piece`: шт · штука · штук · штуки · од · одиниця · pcs · pc · piece · unit
- `kg`: кг · кілограм · килограмм · кілограмів · kg
- `ton`: т · t · тонна · тонн · тонни · ton · tonne
- `liter`: л · l · літр · литр · літрів · liter · litre
- `bag`: мішок · мешок · мішків · bag
- `pack`: уп · пак · пачка · упаковка · паковання · pack
- `canister`: каністра · канистра · кан · canister
- `bottle`: пляшка · бутылка · флакон · bottle
- `set`: набір · набор · set
- `hectare`: га · гектар · гектарів · ha · hectare

## Лимиты и идемпотентность

- `POST /v1/products` — 5000 × 10 min · 30 req / 10 min
- `PATCH /v1/offers` — 5000 × 60 s · 30 req / 60 s
- 20000 unresolved rows · 10 open jobs
- `sku` + ≥1 of `price`, `stock`, `moq`, `discontinued` per `PATCH /v1/offers` item

Отправляйте `Idempotency-Key` на каждом настоящем вызове. Повтор с тем же ключом возвращает сохранённый результат — то же задание, те же отклонения — и ничего не создаёт, так что таймаут на вашей стороне никогда не станет вторым прайсом. Повтор несёт первые 20 отклонений и rejected_total; остальное берите из эндпоинта отклонений.

## Открыть в своём инструменте

Контракт выше также опубликован как документ OpenAPI 3.1, сгенерированный из того же источника — поэтому он не может разойтись с этой страницей. Наведите на него любой клиент и получите конструктор запросов, валидацию и консоль `Try it`, которую мы сами не пишем.

```
https://adam.ua/ru/docs/api/openapi.json
```

- Postman · Insomnia · Bruno — Import → Link, вставьте URL
- Swagger Editor — File → Import URL
- Генераторы клиентов — openapi-generator, oazapfts, openapi-typescript

Чтобы отправить настоящий запрос, нужен рабочий ключ. Делайте это в Настройки → Интеграции → Песочница, где ключ остаётся в вашей сессии, а не на странице, которую может открыть кто угодно.

## Что происходит дальше

Принятые строки сопоставляются с нашим каталогом и попадают на ваш экран проверки. Позицию, которую мы распознали, вы подтверждаете там; ту, которую не распознали, становится заявкой на новый товар каталога. Проверка — шаг человека, и он не мгновенный: планируйте первую выгрузку с учётом этого. Это также однократно: когда позиция связана, её цена и остаток идут через `PATCH /v1/offers` и больше никогда не встают в очередь на проверку.

## Пока не поддерживается

`currency`, `vat_rate` и `lead_time_days` не читаются ни на одном из эндпоинтов, поэтому их отправка — ошибка, а не тихое отбрасывание. Ступени цены по количеству через API тоже не редактируются: предложение со ступенями отклоняет фиксированную `price` как `price_tiered`.
