{"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":"What the product is called in your system. Trimmed, non-empty."},"brand":{"type":"string","maxLength":120,"description":"Resolved within the category by the matcher."},"category":{"type":"string","maxLength":120,"description":"Human form. Resolved against the slugs and aliases listed below."},"unit":{"type":"string","maxLength":40,"description":"Human form: «шт», «шт.», «кг», «т», «каністра» all resolve."},"price":{"type":"number","exclusiveMinimum":0,"description":"Greater than 0. A number, not a string; `.` as the decimal separator."},"stock":{"type":"number","minimum":0,"description":"0 or more. Zero means out of stock, not unknown."},"sku":{"type":"string","maxLength":100,"description":"Your own article number. Optional by schema, but without it a later PATCH has nothing to resolve against."},"warehouse":{"type":"string","maxLength":200,"description":"Matched against your own warehouses — first the exact label or city, then the one warehouse that contains the value. No match or no value at all falls back to your default: your only warehouse, or the one you marked primary. Two candidates is the one case we refuse to guess: the row keeps no warehouse and reports `warehouse_ambiguous`."},"moq":{"type":"number","exclusiveMinimum":0,"description":"Greater than 0. Defaults to 1."},"description":{"type":"string","maxLength":500,"description":"Free text. Used when a row becomes a request for a new catalogue product."},"updated_at":{"type":"string","format":"date-time","description":"When the item last changed in your system. Enables the ordering rule on 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":"Your own article number, as sent to POST /v1/products. The only way to name the position."},"price":{"type":"number","exclusiveMinimum":0,"description":"Greater than 0. Omit it and the price stays as it is."},"stock":{"type":"number","minimum":0,"description":"0 or more. Zero sets the offer to out of stock and leaves it listed."},"moq":{"type":"number","exclusiveMinimum":0,"description":"Greater than 0. The minimum order quantity; the unit is not changed here."},"discontinued":{"type":"boolean","description":"true withdraws the offer, false puts it back. Withdrawal always succeeds; relisting can be refused."},"updated_at":{"type":"string","format":"date-time","description":"When the position last changed in your system. Send it and an out-of-order retry cannot overwrite a newer price; omit it and the last request to arrive wins. Equal to the stored one with the same values is a no-op; equal with different values is 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":"The body is not an object with an items array."},{"code":"unknown_field","scope":"request","endpoints":["products","offers","feed"],"meaning":"A field we do not read. Sent back rather than dropped, so you never believe you set something you did not."},{"code":"idempotency_key_reused","scope":"request","endpoints":["products","offers","feed"],"meaning":"This Idempotency-Key was already used for a request with a different body. Use a new key — the stored result belongs to the earlier request, not this one."},{"code":"no_items","scope":"request","endpoints":["products","offers","feed"],"meaning":"items is empty."},{"code":"too_many_items","scope":"request","endpoints":["products","offers","feed"],"meaning":"More items than the per-request limit. Send fewer."},{"code":"required_field_missing","scope":"row","endpoints":["products","offers","feed"],"meaning":"Carries `field` — the required field that was absent or blank. On PATCH /v1/offers that is always sku."},{"code":"field_too_long","scope":"row","endpoints":["products","offers","feed"],"meaning":"Carries `field`. The bound for each field is in the table above."},{"code":"price_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Not a number, or not greater than 0."},{"code":"stock_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Not a number, or negative. Zero is valid."},{"code":"moq_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Not a number, or not greater than 0."},{"code":"sku_duplicate_in_payload","scope":"row","endpoints":["products","offers","feed"],"meaning":"The same SKU appears earlier in this request. The first occurrence wins."},{"code":"updated_at_invalid","scope":"row","endpoints":["products","offers","feed"],"meaning":"Not RFC 3339, for example 2026-08-24T14:02:31Z."},{"code":"review_backlog","scope":"request","endpoints":["products"],"meaning":"Your unreviewed rows are at the limit. Finish reviewing before sending more."},{"code":"category_unresolved","scope":"row","endpoints":["products"],"meaning":"The value matches no slug and no alias."},{"code":"unit_unresolved","scope":"row","endpoints":["products"],"meaning":"The value matches no unit."},{"code":"wrong_endpoint","scope":"row","endpoints":["products"],"meaning":"You sent `discontinued` here. Withdrawal and relisting belong to PATCH /v1/offers."},{"code":"no_change_requested","scope":"row","endpoints":["offers","feed"],"meaning":"sku and nothing changeable. A timestamp alone is not a change — send price, stock, moq or discontinued."},{"code":"discontinued_invalid","scope":"row","endpoints":["offers","feed"],"meaning":"Present but not a boolean. `\"yes\"` is not true."},{"code":"sku_unknown","scope":"row","endpoints":["offers","feed"],"meaning":"No live mapping for this code. Send the position through POST /v1/products once; after it is confirmed, PATCH works forever."},{"code":"sku_pending_catalog","scope":"row","endpoints":["offers","feed"],"meaning":"You already sent this code and the position is waiting for our catalogue team. Do not resend it — PATCH starts working on this code the moment the product is created."},{"code":"sku_ambiguous","scope":"row","endpoints":["offers","feed"],"meaning":"More than one of your offers carries this code, so we cannot tell which price to move. `detail` names the products. Give them distinct codes in your own system."},{"code":"offer_missing","scope":"row","endpoints":["offers","feed"],"meaning":"The mapping exists but its offer is gone. Send the position through POST /v1/products again."},{"code":"stale_update","scope":"row","endpoints":["offers","feed"],"meaning":"Your updated_at is older than the last one we applied, so nothing changed. `detail` carries the timestamp we hold."},{"code":"equal_timestamp_conflict","scope":"row","endpoints":["offers","feed"],"meaning":"Your updated_at equals the one we already applied, but the values differ — so we cannot tell which is current and refuse rather than guess. Advance updated_at. If the values match, the item is counted in `unchanged` instead and no error is returned."},{"code":"relist_blocked","scope":"row","endpoints":["offers","feed"],"meaning":"`discontinued: false` was refused by the publish gate. Withdrawal always succeeds; relisting needs the offer to be complete and your account able to publish. `detail` carries the reason."},{"code":"price_tiered","scope":"row","endpoints":["offers","feed"],"meaning":"This offer has a quantity price ladder, and a single flat price cannot replace it. Remove the ladder in the dashboard, then the API manages this position."},{"code":"bad_url","scope":"request","endpoints":["feed"],"meaning":"The stored URL is no longer valid. Enter it again."},{"code":"bad_pin","scope":"request","endpoints":["feed"],"meaning":"We could not read the address stored for this feed. Check the address again and save to replace it."},{"code":"pin_unresolvable","scope":"request","endpoints":["feed"],"meaning":"Your hostname does not resolve at all right now. Check your DNS, then check the address again."},{"code":"host_not_routable","scope":"request","endpoints":["feed"],"meaning":"Your hostname resolves, but every address it currently publishes is one we will not connect to — private, loopback, carrier-NAT, link-local or reserved space. Point the hostname at a public address."},{"code":"bad_auth","scope":"request","endpoints":["feed"],"meaning":"The token stored for this feed could not be used. Enter it again and save."},{"code":"too_large","scope":"request","endpoints":["feed"],"meaning":"The response is larger than we accept. Split it or drop the fields you do not need."},{"code":"timeout","scope":"request","endpoints":["feed"],"meaning":"Your feed did not answer in time."},{"code":"fetch_failed","scope":"request","endpoints":["feed"],"meaning":"We could not connect to your feed."},{"code":"feed_http_error","scope":"request","endpoints":["feed"],"meaning":"Your feed answered with an HTTP error. A 401 or 403 means the feed refused our token."},{"code":"bad_encoding","scope":"request","endpoints":["feed"],"meaning":"The response was not valid UTF-8. Serve the file as UTF-8."},{"code":"malformed_json","scope":"request","endpoints":["feed"],"meaning":"The response is not valid JSON."},{"code":"payload_too_large","scope":"request","endpoints":["feed"],"meaning":"One batch of your feed was too large for us to apply. Drop the fields you do not need, or split the file."},{"code":"feed_not_found","scope":"request","endpoints":["feed"],"meaning":"The feed record went missing while the run was in progress. Save the feed again; if it keeps happening, contact support."},{"code":"lease_expired","scope":"request","endpoints":["feed"],"meaning":"We started a run and had to drop it before it finished. Nothing is wrong with your feed — the next scheduled run will try again."},{"code":"lease_lost","scope":"request","endpoints":["feed"],"meaning":"Another run picked this feed up while we were applying it, so we rolled back and nothing changed. Nothing is wrong with your feed — the next scheduled run will try again."},{"code":"internal_error","scope":"request","endpoints":["feed"],"meaning":"Something broke on our side while applying your feed. Nothing is wrong with your feed — the next scheduled run will try again."},{"code":"lock_timeout","scope":"request","endpoints":["feed"],"meaning":"Something broke on our side while applying your feed. Nothing is wrong with your feed — the next scheduled run will try again."},{"code":"statement_timeout","scope":"request","endpoints":["feed"],"meaning":"Something broke on our side while applying your feed. Nothing is wrong with your feed — the next scheduled run will try again."}]}