{"openapi":"3.1.0","info":{"title":"ADAM vendor ingest API","version":"1.0.0","description":"Push your product feed to ADAM. A new position reaches a review queue once; after it is linked, PATCH /v1/offers moves its price and stock with no review. The same offers contract also runs in reverse: see the offerFeedPull webhook, where ADAM fetches the identical payload from a URL you host, once a day."},"servers":[{"url":"https://ingest.adam.ua","description":"ingest service"}],"externalDocs":{"url":"https://adam.ua/docs/api","description":"Field-by-field guide"},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"The key issued in Settings → Integrations. Shown once at creation."}},"schemas":{"ProductItem":{"type":"object","required":["name","brand","category","unit","price","stock"],"additionalProperties":false,"properties":{"name":{"type":"string","maxLength":300,"description":"Название товара в вашей системе. Обрезается, не может быть пустым."},"brand":{"type":"string","maxLength":120,"description":"Распознаётся матчером внутри категории."},"category":{"type":"string","maxLength":120,"description":"В человеческом виде. Распознаётся по списку ниже."},"unit":{"type":"string","maxLength":40,"description":"В человеческом виде: «шт», «шт.», «кг», «т», «канистра» — все распознаются."},"price":{"type":"number","exclusiveMinimum":0,"description":"Больше 0. Число, не строка; десятичный разделитель — «.»."},"stock":{"type":"number","minimum":0,"description":"0 или больше. Ноль означает «нет в наличии», а не «неизвестно»."},"sku":{"type":"string","maxLength":100,"description":"Ваш артикул. По схеме необязателен, но без него последующий PATCH не имеет по чему искать позицию."},"warehouse":{"type":"string","maxLength":200,"description":"Сверяется с вашими складами — сначала точное название или город, затем единственный склад, название которого содержит это значение. Если не совпало или значения нет вовсе, строка ляжет на ваш склад по умолчанию: единственный ваш склад либо тот, который вы отметили основным. Два кандидата — единственный случай, когда мы не угадываем: склад не проставляется, возвращается `warehouse_ambiguous`."},"moq":{"type":"number","exclusiveMinimum":0,"description":"Больше 0. По умолчанию 1."},"description":{"type":"string","maxLength":500,"description":"Свободный текст. Используется, когда строка становится заявкой на новый товар каталога."},"updated_at":{"type":"string","format":"date-time","description":"Когда позиция последний раз менялась в вашей системе. Даёт правило порядка для PATCH."}}},"OfferItem":{"type":"object","required":["sku"],"additionalProperties":false,"minProperties":2,"description":"sku plus at least one of price, stock, moq, discontinued. sku alone changes nothing and is rejected as no_change_requested.","properties":{"sku":{"type":"string","maxLength":100,"description":"Ваш артикул, тот же, что отправляли в POST /v1/products. Единственный способ назвать позицию."},"price":{"type":"number","exclusiveMinimum":0,"description":"Больше 0. Не отправляйте — цена останется как есть."},"stock":{"type":"number","minimum":0,"description":"0 или больше. Ноль ставит «нет в наличии» и оставляет предложение в каталоге."},"moq":{"type":"number","exclusiveMinimum":0,"description":"Больше 0. Минимальная партия; единица измерения здесь не меняется."},"discontinued":{"type":"boolean","description":"true снимает предложение с продажи, false возвращает. Снятие всегда проходит; возврат может быть отклонён."},"updated_at":{"type":"string","format":"date-time","description":"Когда позиция последний раз менялась в вашей системе. Отправьте — и запоздавший повтор не перезапишет более новую цену; не отправите — победит последний пришедший запрос. То же время и те же значения — это no-op; то же время с другими значениями — equal_timestamp_conflict."}}},"Rejection":{"type":"object","required":["index","reason"],"properties":{"index":{"type":"integer","minimum":0,"description":"Your own position in items."},"sku":{"type":["string","null"]},"reason":{"type":"string","enum":["required_field_missing","field_too_long","price_invalid","stock_invalid","moq_invalid","sku_duplicate_in_payload","updated_at_invalid","category_unresolved","unit_unresolved","wrong_endpoint","no_change_requested","discontinued_invalid","sku_unknown","sku_pending_catalog","sku_ambiguous","offer_missing","stale_update","equal_timestamp_conflict","relist_blocked","price_tiered"]},"field":{"type":"string"},"detail":{"type":"string"}}}}},"security":[{"apiKey":[]}],"paths":{"/v1/whoami":{"get":{"summary":"Confirm a key works","responses":{"200":{"description":"The key is live","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"vendor_slug":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"server_time":{"type":"string","format":"date-time"}}}}}},"401":{"description":"No key, or a revoked one"}}}},"/v1/products":{"post":{"summary":"Send a product feed","description":"At most 5000 items per request, 30 requests per 10 minutes. The response carries the first 20 rejections plus rejected_total; fetch the rest from /v1/requests/{id}/rejections, which pages the first 1000 we store.","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean"},"description":"Validate and report without writing anything."},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":16,"maxLength":255},"description":"Required unless dry_run=true. A repeat returns the stored result."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"additionalProperties":false,"properties":{"items":{"type":"array","minItems":1,"maxItems":5000,"items":{"$ref":"#/components/schemas/ProductItem"}}}},"example":{"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"}]}}}},"responses":{"200":{"description":"Same shape, for dry_run=true — nothing was written"},"202":{"description":"Accepted for processing","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"job_id":{"type":["string","null"],"format":"uuid"},"accepted":{"type":"integer","minimum":0},"rejected":{"type":"array","items":{"$ref":"#/components/schemas/Rejection"}},"rejected_total":{"type":"integer","minimum":0}}},"example":{"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}}}},"400":{"description":"See the request-level reason codes"},"401":{"description":"No key, or a revoked one"},"403":{"description":"The key lacks products:write, or the vendor is inactive"},"409":{"description":"review_backlog — unreviewed rows are at the limit, or idempotency_key_reused"},"413":{"description":"The body exceeds 16777216 bytes"},"429":{"description":"rate_limited — carries Retry-After"}}}},"/v1/offers":{"patch":{"summary":"Update price and stock on positions already linked","description":"At most 5000 items per request, 30 requests per 60 seconds. No review queue: an item whose sku resolves to exactly one of your offers is applied immediately. not_published counts rows applied to offers a farmer cannot see — a draft stays a draft.","parameters":[{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean"},"description":"Judge every item and report, without writing anything."},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":16,"maxLength":255},"description":"Required unless dry_run=true. A repeat returns the stored result."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"additionalProperties":false,"properties":{"items":{"type":"array","minItems":1,"maxItems":5000,"items":{"$ref":"#/components/schemas/OfferItem"}}}},"example":{"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}]}}}},"responses":{"200":{"description":"Same shape, for dry_run=true — nothing was written"},"202":{"description":"Applied","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"applied":{"type":"integer","minimum":0},"unchanged":{"type":"integer","minimum":0,"description":"Items whose offer already held the values sent. Nothing was written and nothing is wrong — a retried identical batch reports these instead of rejections."},"not_published":{"type":"integer","minimum":0,"description":"Of the applied rows, how many sit on an offer a farmer cannot see."},"rejected":{"type":"array","items":{"$ref":"#/components/schemas/Rejection"}},"rejected_total":{"type":"integer","minimum":0}}},"example":{"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}}}},"400":{"description":"See the request-level reason codes"},"401":{"description":"No key, or a revoked one"},"403":{"description":"The key lacks offers:write, or the vendor is inactive"},"409":{"description":"idempotency_key_reused — this Idempotency-Key was used for a different body"},"413":{"description":"The body exceeds 16777216 bytes"},"429":{"description":"rate_limited — carries Retry-After. Dry runs are metered too, against their own count"}}}},"/v1/requests/{request_id}/rejections":{"get":{"summary":"Page through one request's rejections","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"One page, ordered by your own item index","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"rejected_total":{"type":"integer","minimum":0},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1},"rejected":{"type":"array","items":{"$ref":"#/components/schemas/Rejection"}}}}}}},"401":{"description":"No key, or a revoked one"},"404":{"description":"No such request for this vendor"}}}}},"webhooks":{"offerFeedPull":{"get":{"summary":"ADAM fetches your offer feed once a day","description":"You host this; ADAM calls it. The response body is byte-for-byte the PATCH /v1/offers request body, and every offers reason code applies unchanged. Configure the URL, the hour, the time zone and the optional token in Settings → Integrations. This transport cannot create products: an article ADAM does not know is rejected sku_unknown, and new positions still go through POST /v1/products.","externalDocs":{"url":"https://adam.ua/docs/api","description":"Requirements in prose"},"parameters":[{"name":"Authorization","in":"header","required":false,"description":"Sent only when you store a token. Exactly `Bearer <token>`; the token is held encrypted and never reaches a log or a process argument.","schema":{"type":"string","pattern":"^Bearer .+$"}}],"responses":{"200":{"description":"The only status ADAM treats as a feed. 204, 206 and every 3xx are recorded as feed_http_error with the status you returned.","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":5000,"items":{"$ref":"#/components/schemas/OfferItem"}}}},"example":{"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}]}}}}},"x-adam-url-requirements":{"scheme":"https","port":443,"portNote":"443 or no port at all. ADAM pins the resolved address with `curl --resolve host:443:<address>`, and that pin covers the pair (host, port) — a URL on another port falls through to ordinary DNS at fetch time and defeats the pin, so it is refused when you save it.","host":"registrable hostname","hostNote":"No IP literals, and no hexadecimal, octal or integer form of one — a client reads those as an address and never resolves them.","tls":"certificate must be valid for that hostname","tlsNote":"The subject is verified, not only the chain and the expiry. A certificate issued for another name is refused.","redirects":0,"redirectsNote":"A 3xx is reported as feed_http_error. Serve the file at the URL you gave.","encoding":"UTF-8","encodingNote":"A body that is not valid UTF-8 is bad_encoding. Windows-1251 is the common mistake in exports from Ukrainian and Russian accounting systems.","maxBytes":16777216,"maxBytesNote":"Above this the fetch stops and the run is too_large.","timeoutSeconds":20,"timeoutNote":"Resolving the hostname has its own six-second budget before this one.","cacheNote":"Do not cache the feed for hours behind a CDN. ADAM fetches once a day, so a long Cache-Control max-age means it reads yesterday's file and your prices look stuck."},"x-adam-address-rule":"The address your hostname resolved to is recorded when you save. Every later fetch resolves again and connects only to an address your own DNS publishes for that hostname at that moment, preferring the recorded one when it is still among them. Moving hosting needs no action from you once DNS is updated. If every published address is private, loopback, carrier-NAT, link-local or reserved, the run stops with host_not_routable; if it publishes none, pin_unresolvable.","x-adam-schedule":"Once a day, at an hour from 00 to 23 in a time zone you choose, so 03:00 means 03:00 where your system runs and the summer-time switch is handled for you. Pick the hour by which the export is already written."}}},"x-adam-units":[{"unit":"piece","accepts":["шт","штука","штук","штуки","од","одиниця","pcs","pc","piece","unit"]},{"unit":"kg","accepts":["кг","кілограм","килограмм","кілограмів","kg"]},{"unit":"ton","accepts":["т","t","тонна","тонн","тонни","ton","tonne"]},{"unit":"liter","accepts":["л","l","літр","литр","літрів","liter","litre"]},{"unit":"bag","accepts":["мішок","мешок","мішків","bag"]},{"unit":"pack","accepts":["уп","пак","пачка","упаковка","паковання","pack"]},{"unit":"canister","accepts":["каністра","канистра","кан","canister"]},{"unit":"bottle","accepts":["пляшка","бутылка","флакон","bottle"]},{"unit":"set","accepts":["набір","набор","set"]},{"unit":"hectare","accepts":["га","гектар","гектарів","ha","hectare"]}],"x-adam-reason-codes":[{"code":"invalid_envelope","scope":"request","endpoints":["products","offers","feed"],"meaning":"Тело запроса — не объект с массивом items."},{"code":"unknown_field","scope":"request","endpoints":["products","offers","feed"],"meaning":"Поле, которое мы не читаем. Возвращаем ошибку, а не молча отбрасываем — чтобы вы не считали, что задали то, чего не задали."},{"code":"idempotency_key_reused","scope":"request","endpoints":["products","offers","feed"],"meaning":"Этот Idempotency-Key уже использован для запроса с другим телом. Возьмите новый ключ — сохранённый результат принадлежит предыдущему запросу, а не этому."},{"code":"no_items","scope":"request","endpoints":["products","offers","feed"],"meaning":"items пуст."},{"code":"too_many_items","scope":"request","endpoints":["products","offers","feed"],"meaning":"Позиций больше лимита на запрос. Отправьте меньшими партиями."},{"code":"required_field_missing","scope":"row","endpoints":["products","offers","feed"],"meaning":"Содержит `field` — обязательное поле, которого нет или оно пустое. На PATCH /v1/offers это всегда sku."},{"code":"field_too_long","scope":"row","endpoints":["products","offers","feed"],"meaning":"Содержит `field`. Граница для каждого поля — в таблице выше."},{"code":"price_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Не число или не больше 0."},{"code":"stock_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Не число или отрицательное. Ноль допустим."},{"code":"moq_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Не число или не больше 0."},{"code":"sku_duplicate_in_payload","scope":"row","endpoints":["products","offers","feed"],"meaning":"Тот же артикул уже есть раньше в этом запросе. Побеждает первое вхождение."},{"code":"updated_at_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Не RFC 3339, например 2026-08-24T14:02:31Z."},{"code":"review_backlog","scope":"request","endpoints":["products"],"meaning":"Ваши непроверенные строки достигли лимита. Завершите проверку, прежде чем отправлять ещё."},{"code":"category_unresolved","scope":"row","endpoints":["products"],"meaning":"Значение не совпадает ни со slug, ни с псевдонимом."},{"code":"unit_unresolved","scope":"row","endpoints":["products"],"meaning":"Значение не совпадает ни с одной единицей."},{"code":"wrong_endpoint","scope":"row","endpoints":["products"],"meaning":"Вы отправили `discontinued` сюда. Снятие с продажи и возврат — это PATCH /v1/offers."},{"code":"no_change_requested","scope":"row","endpoints":["offers","feed"],"meaning":"Только sku, без изменяемых полей. Один timestamp — не изменение: отправьте price, stock, moq или discontinued."},{"code":"discontinued_invalid","scope":"row","endpoints":["offers","feed"],"meaning":"Присутствует, но не булево. `\"yes\"` — это не true."},{"code":"sku_unknown","scope":"row","endpoints":["offers","feed"],"meaning":"Для этого кода нет активной связи. Отправьте позицию через POST /v1/products один раз — после подтверждения PATCH работает всегда."},{"code":"sku_pending_catalog","scope":"row","endpoints":["offers","feed"],"meaning":"Вы уже отправляли этот код, и позиция ждёт нашу каталожную команду. Не отправляйте повторно — PATCH начнёт работать с этим кодом сразу после создания товара."},{"code":"sku_ambiguous","scope":"row","endpoints":["offers","feed"],"meaning":"Этот код есть у нескольких ваших предложений, поэтому неясно, какую цену менять. `detail` перечисляет товары. Дайте им разные коды в своей системе."},{"code":"offer_missing","scope":"row","endpoints":["offers","feed"],"meaning":"Связь есть, но её предложение исчезло. Отправьте позицию через POST /v1/products ещё раз."},{"code":"stale_update","scope":"row","endpoints":["offers","feed"],"meaning":"Ваш updated_at старее последнего применённого, поэтому ничего не изменилось. `detail` содержит наш timestamp."},{"code":"equal_timestamp_conflict","scope":"row","endpoints":["offers","feed"],"meaning":"Ваш updated_at совпадает с уже применённым, но значения другие — мы не можем определить, какое актуально, и отказываем, а не угадываем. Сдвиньте updated_at. Если значения совпадают, позиция идёт в `unchanged` и ошибки нет."},{"code":"relist_blocked","scope":"row","endpoints":["offers","feed"],"meaning":"`discontinued: false` отклонён проверкой публикации. Снятие с продажи проходит всегда; возврат требует полного предложения и права публиковать. `detail` содержит причину."},{"code":"price_tiered","scope":"row","endpoints":["offers","feed"],"meaning":"У этого предложения есть ступени цены по количеству, и одна фиксированная цена их не заменяет. Уберите ступени в кабинете — дальше позицией управляет API."},{"code":"bad_url","scope":"request","endpoints":["feed"],"meaning":"Сохранённый URL больше не действителен. Введите его заново."},{"code":"bad_pin","scope":"request","endpoints":["feed"],"meaning":"Не удалось прочитать сохранённый адрес. Проверьте его ещё раз и сохраните, чтобы заменить."},{"code":"pin_unresolvable","scope":"request","endpoints":["feed"],"meaning":"Ваше доменное имя сейчас вообще не разрешается. Проверьте свой DNS, затем проверьте адрес ещё раз."},{"code":"host_not_routable","scope":"request","endpoints":["feed"],"meaning":"Ваше доменное имя разрешается, но все адреса, которые оно сейчас публикует, — те, к которым мы не обращаемся: приватные, loopback, carrier-NAT, link-local или зарезервированные. Направьте доменное имя на публичный адрес."},{"code":"bad_auth","scope":"request","endpoints":["feed"],"meaning":"Сохранённый токен не удалось использовать. Введите его заново и сохраните."},{"code":"too_large","scope":"request","endpoints":["feed"],"meaning":"Ответ больше, чем мы принимаем. Разделите его или уберите ненужные поля."},{"code":"timeout","scope":"request","endpoints":["feed"],"meaning":"Ваш сервер не ответил за отведённое время."},{"code":"fetch_failed","scope":"request","endpoints":["feed"],"meaning":"Не удалось соединиться с вашим сервером."},{"code":"feed_http_error","scope":"request","endpoints":["feed"],"meaning":"Ваш сервер ответил HTTP-ошибкой. Код 401 или 403 означает, что он не принял наш токен."},{"code":"bad_encoding","scope":"request","endpoints":["feed"],"meaning":"Ответ не является корректным UTF-8. Отдавайте файл в UTF-8."},{"code":"malformed_json","scope":"request","endpoints":["feed"],"meaning":"Ответ не является корректным JSON."},{"code":"payload_too_large","scope":"request","endpoints":["feed"],"meaning":"Одна партия вашей выгрузки была слишком большой, чтобы мы могли её применить. Уберите ненужные поля или разделите файл."},{"code":"feed_not_found","scope":"request","endpoints":["feed"],"meaning":"Настройки пропали, пока запуск ещё выполнялся. Сохраните их заново; если это повторяется, обратитесь в поддержку."},{"code":"lease_expired","scope":"request","endpoints":["feed"],"meaning":"Мы начали запуск и вынуждены были прервать его до завершения. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова."},{"code":"lease_lost","scope":"request","endpoints":["feed"],"meaning":"Это же задание параллельно подхватил другой запуск, поэтому мы откатили изменения и ничего не применили. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова."},{"code":"internal_error","scope":"request","endpoints":["feed"],"meaning":"Что-то сломалось на нашей стороне при применении данных. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова."},{"code":"lock_timeout","scope":"request","endpoints":["feed"],"meaning":"Что-то сломалось на нашей стороне при применении данных. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова."},{"code":"statement_timeout","scope":"request","endpoints":["feed"],"meaning":"Что-то сломалось на нашей стороне при применении данных. С вашей выгрузкой всё в порядке — следующий запланированный запуск попробует снова."}]}