API

Send your price list to ADAM

One HTTP call replaces the spreadsheet. Your system posts the feed, we tell you row by row what we read and what we could not.

Validate a payload without creating anything in Settings → Integrations → Sandbox. OpenAPI document · Download as Markdown — for AI agents

Getting a key

Settings → Integrations → Connect. The secret is shown once at creation and stored only as a hash, so keep it where your ERP can read it. Send it as 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"}]}'

The item, field by field

FieldTypeRule
namestring · ≤300
required
What the product is called in your system. Trimmed, non-empty.
brandstring · ≤120
required
Resolved within the category by the matcher.
categorystring · ≤120
required
Human form. Resolved against the slugs and aliases listed below.
unitstring · ≤40
required
Human form: «шт», «шт.», «кг», «т», «каністра» all resolve.
pricenumber
required
Greater than 0. A number, not a string; `.` as the decimal separator.
stocknumber
required
0 or more. Zero means out of stock, not unknown.
skustring · ≤100
optional
Your own article number. Optional by schema, but without it a later PATCH has nothing to resolve against.
warehousestring · ≤200
optional
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`.
moqnumber
optional
Greater than 0. Defaults to 1.
descriptionstring · ≤500
optional
Free text. Used when a row becomes a request for a new catalogue product.
updated_atrfc3339
optional
When the item last changed in your system. Enables the ordering rule on PATCH.

A field we do not read fails the whole request instead of being dropped. Dropping it quietly is how you come to believe you set a currency you did not set.

The response

202, because rows are accepted for processing with matching and review still ahead. Rejections name your own item index, so a feed you fix one pass at a time converges.

{
  "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
}

Reason codes

CodeScopeMeaning
invalid_envelopewhole request

The body is not an object with an items array.

unknown_fieldwhole request

A field we do not read. Sent back rather than dropped, so you never believe you set something you did not.

idempotency_key_reusedwhole request

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.

no_itemswhole request

items is empty.

too_many_itemswhole request

More items than the per-request limit. Send fewer.

required_field_missingone row

Carries field — the required field that was absent or blank. On PATCH /v1/offers that is always sku.

field_too_longone row

Carries field. The bound for each field is in the table above.

price_invalidone row

Not a number, or not greater than 0.

stock_invalidone row

Not a number, or negative. Zero is valid.

moq_invalidone row

Not a number, or not greater than 0.

sku_duplicate_in_payloadone row

The same SKU appears earlier in this request. The first occurrence wins.

updated_at_invalidone row

Not RFC 3339, for example 2026-08-24T14:02:31Z.

review_backlogwhole request

Your unreviewed rows are at the limit. Finish reviewing before sending more.

category_unresolvedone row

The value matches no slug and no alias.

unit_unresolvedone row

The value matches no unit.

wrong_endpointone row

You sent discontinued here. Withdrawal and relisting belong to PATCH /v1/offers.

A closed set, so your integration can branch on them. One code per rejected row: a row failing several checks reports the first in the order above.

Updating price and stock

Once a position is linked, PATCH /v1/offers moves its price and stock with no review and no human step. You send the article number you already sent to the product feed; we change the offer behind it. This is the call your ERP runs on a schedule.

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}]}'
FieldTypeRule
skustring · ≤100
required
Your own article number, as sent to POST /v1/products. The only way to name the position.
pricenumber
optional
Greater than 0. Omit it and the price stays as it is.
stocknumber
optional
0 or more. Zero sets the offer to out of stock and leaves it listed.
moqnumber
optional
Greater than 0. The minimum order quantity; the unit is not changed here.
discontinuedboolean
optional
true withdraws the offer, false puts it back. Withdrawal always succeeds; relisting can be refused.
updated_atrfc3339
optional
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.

An item needs its sku plus at least one of price, stock, moq, discontinued. Everything you leave out stays as it is, so a stock-only sweep never touches a price. sku on its own is rejected rather than counted as applied.

Send updated_at and retries stop being dangerous: an item whose timestamp is older than the last one we applied changes nothing and comes back as stale_update. Omit it and the last request to arrive wins, which is fine for a single scheduled job and wrong for two.

unchanged counts items whose offer already held the values you sent. Nothing was written and nothing is wrong — a nightly feed that resends everything gets applied for what moved and unchanged for the rest, instead of thousands of errors. Sending the same updated_at with the same values lands here too; sending it with different values is equal_timestamp_conflict, because equal timestamps cannot say which version is current.

Withdrawal and relisting are not symmetric, and it is better you know why than discover it. discontinued: true always succeeds — taking your own offer down needs nobody's permission. discontinued: false puts it back only if the offer is complete and your account may publish; otherwise the row comes back as relist_blocked with the reason in detail.

not_published is the number of applied rows sitting on an offer a farmer cannot see — a draft, a paused offer. The price did change; nobody is looking at it. A patch never publishes on your behalf, so this count is how you find out rather than wondering why nothing moved.

{
  "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
}
CodeScopeMeaning
no_change_requestedone row

sku and nothing changeable. A timestamp alone is not a change — send price, stock, moq or discontinued.

discontinued_invalidone row

Present but not a boolean. "yes" is not true.

sku_unknownone row

No live mapping for this code. Send the position through POST /v1/products once; after it is confirmed, PATCH works forever.

sku_pending_catalogone row

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.

sku_ambiguousone row

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.

offer_missingone row

The mapping exists but its offer is gone. Send the position through POST /v1/products again.

stale_updateone row

Your updated_at is older than the last one we applied, so nothing changed. detail carries the timestamp we hold.

equal_timestamp_conflictone row

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.

relist_blockedone row

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.

price_tieredone row

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.

Only the codes this endpoint alone returns. Every request-level code and every value check in the table above applies here too, with field naming sku where the product feed would name a dozen fields.

Pull — we fetch the feed ourselves

Exactly the same contract as PATCH /v1/offers — the same request structure, the same item fields, the same reason codes, the same effect. The only difference is direction: instead of your system calling us, we call a URL you host, once a day. Nothing else in the pipeline changes. You set it up under Settings → Integrations.

It cannot create products. An article number we do not know is rejected sku_unknown. New positions still go through POST /v1/products and its review queue; pull moves price and stock on positions that are already linked, and nothing more.

The body is the one you already send to PATCH /v1/offers: an items array whose objects carry sku plus price, stock, moq, discontinued and updated_at. At least one of price, stock, moq, discontinued per item, or the item is rejected no_change_requested. An unrecognised field rejects the whole request as unknown_field — deliberately, not as a silent drop. At most 5000 items (too_many_items), and one batch over 16 MiB of JSON is payload_too_large.

Once a day. You pick the hour, 00 to 23, and the time zone it is counted in — so 03:00 means 03:00 where your system runs, and the switch to and from summer time is handled for you. The next run is shown on the card. Pick the hour by which your export is already written, not the hour it starts.

Authentication is optional: either nothing, or one Authorization: Bearer <token> header that you set under Settings → Integrations. The token is stored encrypted, sent to your server on every fetch, and never written into a log or a process argument. A 401 or 403 from you is feed_http_error with that status, not an auth code of ours.

What we require of your URL

https:// only — plain HTTP is refused before any connection is made. Port 443 or no port at all: we pin the address your hostname resolved to with curl --resolve host:443:<address>, and that pin covers the pair (host, port), so a URL on another port would fall through to ordinary DNS at fetch time and defeat the pin entirely. Give a hostname, not an address — no IP literals, and no hexadecimal, octal or integer form of one (https://0x7f000001/ is refused), because a client treats those as an address and never resolves them.

A valid TLS certificate matching that hostname. We verify the certificate's subject, not only its chain and expiry — a certificate valid for a different name is refused. And no redirects: we send --max-redirs 0, so a 3xx comes back to you as feed_http_error with its status. Serve the file at the URL you gave us.

A plain GET — we send no body and no query of our own. The response must be valid UTF-8; anything else is bad_encoding, and Windows-1251 is the common mistake for exports from Ukrainian and Russian accounting systems. 16 MiB is the ceiling: above it the fetch stops and you get too_large. You have 20 seconds to respond; resolving your hostname has its own six-second budget before that. Do not cache the feed for long behind a CDN — we fetch once a day, so a Cache-Control max-age of hours or days means we read yesterday's file and your prices look stuck. Give it a short max-age or none.

When you save the feed we resolve your hostname and record the address. On every later fetch we resolve it again and connect only to an address your own DNS publishes for that hostname right now, preferring the recorded one while it is still among them. Two consequences worth stating: moving hosting needs no action from you as long as DNS is updated, and we will never connect to an address your DNS does not currently name. If every address it publishes is private, loopback, carrier-NAT, link-local or reserved space, the run stops with host_not_routable; if it publishes none at all, pin_unresolvable.

When a run fails

Every run leaves a row in the request journal on the integrations screen, marked as having come from the feed, with the counts and the rejections. A failure you can act on also sends you an in-app notification.

Failures on our side — a lost lease, our database — deliberately send no notification and are labelled as ours, because they are not something you can fix and the next scheduled run retries by itself.

CodeScopeMeaning
bad_urlwhole request

The stored URL is no longer valid. Enter it again.

bad_pinwhole request

We could not read the address stored for this feed. Check the address again and save to replace it.

pin_unresolvablewhole request

Your hostname does not resolve at all right now. Check your DNS, then check the address again.

host_not_routablewhole request

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.

bad_authwhole request

The token stored for this feed could not be used. Enter it again and save.

too_largewhole request

The response is larger than we accept. Split it or drop the fields you do not need.

timeoutwhole request

Your feed did not answer in time.

fetch_failedwhole request

We could not connect to your feed.

feed_http_errorwhole request

Your feed answered with an HTTP error. A 401 or 403 means the feed refused our token.

bad_encodingwhole request

The response was not valid UTF-8. Serve the file as UTF-8.

malformed_jsonwhole request

The response is not valid JSON.

payload_too_largewhole request

One batch of your feed was too large for us to apply. Drop the fields you do not need, or split the file.

feed_not_foundwhole request

The feed record went missing while the run was in progress. Save the feed again; if it keeps happening, contact support.

lease_expiredwhole request

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.

lease_lostwhole request

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.

internal_errorwhole request

Something broke on our side while applying your feed. Nothing is wrong with your feed — the next scheduled run will try again.

lock_timeoutwhole request

Something broke on our side while applying your feed. Nothing is wrong with your feed — the next scheduled run will try again.

statement_timeoutwhole request

Something broke on our side while applying your feed. Nothing is wrong with your feed — the next scheduled run will try again.

Codes only a pull run produces — they describe the fetch itself, before any item is read. Every code in the PATCH /v1/offers tables applies to the payload unchanged, because it is the same payload. The last three are ours, not yours: the next scheduled run retries by itself and you get no notification for them.

Categories we recognise

Read live from the catalogue, so this list cannot drift from what the resolver accepts. Case and trailing punctuation do not matter.

  • seedsSeeds

    Hybrids · Seed · seeds · Sowing material · Гибриды · Гібриди · Кукурудза · Кукуруза · Насіннєвий матеріал · Насіння · Насіння кукурудзи · Насіння рапсу · Насіння сої · Насіння соняшника · Посевной материал · Посівний матеріал · Пшеница · Пшениця · Рапс · Семена · Семена кукурузы · Семена подсолнечника · Семена подсолнуха · Семена рапса · Семена сои · Семенной материал · Соняшник · Соя · Ячмень · Ячмінь

  • fertilizersFertilizers

    Fertiliser · Fertilizer · fertilizers · Growth stimulants · Micro fertilizers · Агрохимия · Агрохімія · Добрива · Микроудобрения · Микроудобрения и стимуляторы роста · Минеральные удобрения · Мікродобрива · Мікродобрива та стимулятори росту · Мінеральні добрива · Органические удобрения · Органічні добрива · Стимулятори росту · Стимуляторы роста · Удобрения

  • pesticidesCrop Protection

    Agrochemicals · CPP · Crop Protection · Fungicide · Fungicides · Herbicide · Herbicides · Insecticide · Insecticides · Pesticide · pesticides · Plant protection · Агрохимикаты · Агрохімікати · Адъюванты · Адьюванти · Акарициди · Акарициды · Гербицид · Гербициды · Гербіцид · Гербіциди · Десиканти · Десиканты · Засоби захисту · Засоби захисту рослин · ЗЗР · Инсектицид · Инсектициды · Інсектицид · Інсектициди · Пестициди · Пестициди та агрохімікати · Пестициды · Пестициды и агрохимикаты · Прилипатели · Прилипачі · Прилипачі (пав) · Протравители · Протруйник · Протруйники · Родентициди · Родентициды · СЗР · Средства защиты · Средства защиты растений · Фунгицид · Фунгициды · Фунгіцид · Фунгіциди

  • fuel-lubricantsFuel & Lubricants

    Diesel · Fuel · fuel-lubricants · Grease · Lubricants · Oils · Антифриз · Бензин · Горюче-смазочные материалы · ГСМ · Дизельне пальне · Дизельное топливо · ДТ · Масла · Мастила · Пальне · Пально-мастильні матеріали · ПММ · Смазки · Топливо

  • machineryMachinery

    Combines · Equipment · Implements · machinery · Tractors · Комбайни · Комбайны · Обладнання · Оборудование · Прицепы · Причепи · Сельхозтехника · Сільгосптехніка · Техника · Техніка · Трактори · Тракторы

  • spare-partsSpare Parts

    Bearings · Belts · Filters · Parts · spare-parts · Детали · Деталі · Запчасти · Запчастини · Ножи · Ножі · Підшипники · Подшипники · Ремені · Ремни · Фильтры · Фільтри

  • tires-wheelsTires & Wheels

    Rims · Tires · tires-wheels · Tyres · Wheels · Диски · Колеса · Колёса · Покришки · Покрышки · Шини · Шини та диски · Шины · Шины и диски

  • suppliesSupplies

    supplies · Витратні матеріали · Расходные материалы · Спецодежда · Спецодяг · Тара · Упаковка

  • servicesServices

    services · Послуги · Сервис · Сервіс · Услуги

  • livestockLivestock

    livestock · Ветпрепарати · Ветпрепараты · Животноводство · Корма · Корми · Тваринництво

  • energy-systemsEnergy Systems

    energy-systems · Генератори · Генераторы · Енергосистеми · Солнечные панели · Сонячні панелі · Энергосистемы

Units we recognise

  • 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

Limits and idempotency

  • 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

Send Idempotency-Key on every real call. A repeat with the same key returns the stored result — the same job, the same rejections — and creates nothing, so a timeout on your side never becomes a second price list. A replay carries the first 20 rejections and rejected_total; page the rest from the rejections endpoint.

Open it in your own tool

The contract above is also published as an OpenAPI 3.1 document, generated from the same source — so it cannot drift from this page. Point any client at it and you get request builders, validation and a Try it console without us shipping one.

https://adam.ua/en/docs/api/openapi.json
https://adam.ua/en/docs/api.md
  • Postman · Insomnia · Bruno — Import → Link, paste the URL
  • Swagger Editor — File → Import URL
  • Client generators — openapi-generator, oazapfts, openapi-typescript

Sending a real request needs a live key. Do that from Settings → Integrations → Sandbox, where the key stays in your own session — not from a page anyone can open.

What happens next

Accepted rows are matched against our catalogue and land in your review screen. A position we recognise is confirmed there; one we do not becomes a request for a new catalogue product. Review is a human step and it is not instant — plan your first feed with that in mind. It is also a one-off: once a position is linked, its price and stock travel through PATCH /v1/offers and never queue for review again.

Not supported yet

currency, vat_rate and lead_time_days are not read on either endpoint, so sending them is an error rather than a silent drop. Quantity price ladders are not editable over the API either — an offer that has one refuses a flat price as price_tiered.

+380 (67) 419-07-94

Contentsquare stores its data in the EU (Ireland). Google Analytics transfers data to the United States, under EU Standard Contractual Clauses.