API
Надсилайте свій прайс до ADAM
Один HTTP-запит замінює таблицю. Ваша система надсилає вивантаження, а ми рядок за рядком кажемо, що прочитали й чого не змогли.
Перевірити payload, нічого не створюючи, можна в Налаштування → Інтеграції → Пісочниця. Документ OpenAPI · Завантажити в Markdown — для ШІ-агентів
Як отримати ключ
Налаштування → Інтеграції → Підключити. Секрет показується один раз при створенні й зберігається лише як хеш — тримайте його там, де його читає ваша ERP. Надсилайте як Authorization: Bearer ….
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"}]}'Позиція, поле за полем
| Поле | Тип | Правило |
|---|---|---|
| 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, бо рядки прийняті в обробку, а зіставлення й перевірка ще попереду. Відхилення називають ваш власний індекс позиції, тому вивантаження, яке ви правите поетапно, сходиться.
{
"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
}Коди причин
| Код | Область | Значення |
|---|---|---|
| invalid_envelope | весь запит | Тіло запиту — не об’єкт із масивом items. |
| unknown_field | весь запит | Поле, яке ми не читаємо. Повертаємо помилку, а не тихо відкидаємо — щоб ви не вважали, що задали те, чого не задали. |
| idempotency_key_reused | весь запит | Цей Idempotency-Key уже використано для запиту з іншим тілом. Візьміть новий ключ — збережений результат належить попередньому запиту, а не цьому. |
| no_items | весь запит | items порожній. |
| too_many_items | весь запит | Позицій більше за ліміт на запит. Надішліть меншими партіями. |
| required_field_missing | один рядок | Містить |
| field_too_long | один рядок | Містить |
| 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 | один рядок | Ви надіслали |
Закритий набір — ваша інтеграція може на них розгалужуватися. Один код на відхилений рядок: рядок, що не пройшов кілька перевірок, повідомляє першу за порядком вище.
Оновлення ціни й залишку
Коли позицію зв’язано, PATCH /v1/offers змінює її ціну й залишок без перевірки та без участі людини. Ви надсилаєте той самий артикул, що вже надсилали у вивантаження товарів, а ми змінюємо пропозицію за ним. Це той виклик, який ваша ERP робить за розкладом.
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 — кількість застосованих рядків на пропозиціях, яких фермер не бачить: чернетка, призупинена пропозиція. Ціна справді змінилася, але на неї ніхто не дивиться. Патч ніколи не публікує замість вас, тому цей показник — те, як ви про це дізнаєтеся, а не гадаєте, чому нічого не зрушило.
{
"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
}| Код | Область | Значення |
|---|---|---|
| no_change_requested | один рядок | Лише sku, без жодного змінюваного поля. Один timestamp — не зміна: надішліть price, stock, moq або discontinued. |
| discontinued_invalid | один рядок | Присутнє, але не булеве. |
| sku_unknown | один рядок | Для цього коду немає активного зв’язку. Надішліть позицію через POST /v1/products один раз — після підтвердження PATCH працює завжди. |
| sku_pending_catalog | один рядок | Ви вже надсилали цей код, і позиція очікує на нашу каталожну команду. Не надсилайте повторно — PATCH почне працювати з цим кодом одразу після створення товару. |
| sku_ambiguous | один рядок | Цей код мають кілька ваших пропозицій, тому неясно, яку ціну змінювати. |
| offer_missing | один рядок | Зв’язок є, але його пропозиція зникла. Надішліть позицію через POST /v1/products ще раз. |
| stale_update | один рядок | Ваш updated_at старіший за останній застосований, тому нічого не змінилося. |
| equal_timestamp_conflict | один рядок | Ваш updated_at збігається з уже застосованим, але значення інші — ми не можемо визначити, яке актуальне, і відмовляємо, а не вгадуємо. Змістіть updated_at. Якщо значення однакові, позиція йде в |
| relist_blocked | один рядок |
|
| 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
Надсилайте Idempotency-Key на кожному справжньому виклику. Повтор із тим самим ключем повертає збережений результат — те саме завдання, ті самі відхилення — і нічого не створює, тож таймаут на вашій стороні ніколи не стає другим прайсом. Повтор несе перші 20 відхилень і rejected_total; решту беріть з ендпоїнта відхилень.
Відкрити у своєму інструменті
Контракт вище також опублікований як документ OpenAPI 3.1, згенерований з того самого джерела — тож він не може розійтися з цією сторінкою. Наведіть на нього будь-який клієнт і отримаєте конструктор запитів, валідацію та консоль Try it, якої ми не пишемо самі.
https://adam.ua/docs/api/openapi.json https://adam.ua/docs/api.md
- 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.