IVO
Back to help center
Merchant Integration

Merchant API reference

Full reference for the merchant API: authentication, every endpoint, and what each input and output field means.

Last updated: 4 November 2026

Get an API key

Sign in with a merchant account, open Settings → Integrations, select the API Keys tab, and create a key. Copy it immediately: the complete key is shown only once. You can revoke keys from the same page.

Base URL and authentication

https://a.ivo.md/v1/merchant-api

Send the key with every request using either header:

X-API-Key: <YOUR_API_KEY>
Authorization: Bearer <YOUR_API_KEY>

Always send Accept: application/json, and Content-Type: application/json except for file uploads. Without the Accept header, authentication failures are returned as an HTML error page instead of JSON.

Response envelope

CaseHTTPBody
Success200The endpoint's fields plus "status": "success".
Handled error202, 403, 404, 409, 422, 500"status": "error", a machine-readable message (the error code), and any extra context fields.
Authentication failure403Raised before the controller runs, so it has no status field — only {"message": "api_key_invalid"}. Codes: api_key_missing, api_key_invalid, merchant_not_found, integration_disabled.
Rate limited429120 requests per minute per IP address. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining.

Important: on a 200 response the top-level status field is always the literal string success — it says the call was accepted, nothing more. Endpoints that also have work in flight report it under a key of their own: sync_status on the sync endpoints, and import_status on the import endpoints.

A 202 response is an error envelope whose message says why the answer is not ready yet: queued, processing, or pending_approval.

Check the connection

GET /check

Validates the API key and returns the merchant identity. This is the endpoint to call first when wiring up an integration.

Input: none beyond the API key header.

{
  "merchant_id": "<MERCHANT_ID>",
  "merchant_name": "My Shop",
  "status": "success"
}
Output fieldTypeMeaning
merchant_idstringThe IVO identifier of the merchant this key belongs to. Store it: it never changes and it identifies the account in support requests.
merchant_namestringThe merchant's display name in IVO.
statusstringAlways success. It confirms the key is valid and not revoked; it is not the merchant's account status.

Merchant points

Every offer belongs to a merchant point (a shop, warehouse or pickup location). Use the point ID returned by these endpoints as merchant_point_id. If the account has exactly one point, product sync selects it automatically and you can omit the field.

GET /merchant-points

Returns all points belonging to the authenticated merchant.

{
  "points": [
    {
      "_id": "<POINT_ID>",
      "name": "Main store",
      "contact_person": "Ion Popescu",
      "email": "shop@example.com",
      "phone": "+37360000000",
      "street_id": "<STREET_ID>",
      "number": "12",
      "city_id": "<CITY_ID>",
      "state_id": "<STATE_ID>",
      "country": "MD",
      "zip_code": "MD-2001",
      "status": "approved",
      "is_active": true
    }
  ],
  "status": "success"
}
Output fieldTypeMeaning
points[]._idstringThe point ID. This is the value to send as merchant_point_id.
points[].namestringThe point name. It can also be sent as merchant_point instead of the ID; the match is exact and case-insensitive.
points[].contact_person, email, phonestringWho the courier and IVO support contact at this location.
points[].street_id, number, block, entrance, floor, apartment, intercom, zip_code, city_id, state_id, countrystringThe pickup address. The location IDs are IVO's own street/city/state identifiers.
points[].statusstringpending (waiting for IVO approval), approved, or rejected. Offers at a point that is not approved stay invisible on the site.
points[].is_activebooleanThe merchant's own on/off switch. Offers at an inactive point stay invisible.
points[].rejection_reasonstring, nullWhy IVO rejected the point, when status is rejected.

POST /merchant-points/store

Creates a point. It is always created with status: pending and is_active: true, and stays invisible to buyers until IVO approves it.

Input fieldTypeMeaning
namestringHow the location is named in your system and in the merchant panel.
contact_personstringThe person the courier asks for at pickup.
email, phonestringContact details for this location.
street_id, city_id, state_idstringIVO location identifiers for the address.
number, block, entrance, floor, apartment, intercomstringThe rest of the street address, so the courier reaches the right door.
zip_code, countrystringPostal code and country code.

Any field outside this list is ignored. The response is {"point": { … }, "status": "success"}, where point is the created record including its new _id.

POST /merchant-points/update/{id}

Updates a point owned by the merchant. {id} is the point ID. Accepts every creation field plus is_active (boolean — set it to false to take the location's whole stock offline without deleting anything). Returns the updated point. Unknown ID: point_not_found (404).

POST /merchant-points/delete/{id}

Removes a point owned by the merchant. Returns {"deleted": true, "status": "success"}. The record is soft-deleted, so its history is kept, but the point can no longer be used for offers.

Update an existing offer

POST /product-offer/update

Updates price or stock on an offer that already exists, without running the product import. This is the cheapest and fastest endpoint — use it for regular price and stock refreshes.

{
  "merchant_internal_id": "PRODUCT-123",
  "merchant_point_id": "<POINT_ID>",
  "price": 1999,
  "currency": "MDL",
  "availability": 5,
  "availability_on_order": 0
}
Input fieldTypeMeaning
offer_idstringThe IVO offer ID. Send this or merchant_internal_id. The offer must belong to your merchant account.
merchant_internal_idstringYour own product identifier (SKU), as sent when the offer was created. Searched within your account only.
merchant_point_idstringOnly used together with merchant_internal_id: it narrows the search to one point. Required when the same SKU exists at several points, otherwise the call fails with merchant_internal_id_not_unique.
pricenumberThe new selling price, in currency. An offer with a price of 0 or empty is never shown on the site.
currencystringISO 4217 code. Accepted: MDL (default), EUR, USD, RON, UAH, RUB, GBP. Prices in another currency are converted for display at IVO's stored rate.
availabilityintegerUnits physically in stock at this point and shippable immediately.
availability_on_orderintegerUnits you can supply on order (supplier stock). Keeps the product buyable when availability is 0, with a longer delivery time.
promotionsarrayThe offer's promotional prices, one item per promotion: code, price, quantity. See "Promotional prices" below.

At least one of availability, availability_on_order, price, currency or promotions must be present; sending only an identifier fails with no_fields_to_update. Fields you omit keep their current value. Every change is written to the offer's price/stock history.

{
  "offer": {
    "id": "<OFFER_ID>",
    "merchant_internal_id": "PRODUCT-123",
    "product_id": "<PRODUCT_ID>",
    "merchant_point_id": "<POINT_ID>",
    "condition": "new",
    "quality": null,
    "price": 1999,
    "currency": "MDL",
    "availability": 5,
    "availability_on_order": 0,
    "promotions": []
  },
  "promotion_errors": [],
  "status": "success"
}
Output fieldTypeMeaning
offer.idstringThe offer ID. Reuse it as offer_id to skip the SKU lookup on later calls.
offer.merchant_internal_idstring, nullYour SKU as stored on the offer.
offer.product_idstringThe IVO product (the variant) this offer is attached to.
offer.merchant_point_idstringThe point the offer is stocked at.
offer.conditionstringnew, used or refurbished.
offer.qualitystring, nullFree-text grade used to tell apart several non-new offers of the same product.
offer.price, offer.currency, offer.availability, offer.availability_on_ordernumber / string / integerThe stored values after the update.
offer.promotionsarrayThe offer's prices for the promotions taking prices now: code, price, currency, quantity and sold (units sold at this price).
promotion_errorsarrayThe items of promotions that were refused: code and error. Empty when nothing was refused.

Errors: missing_identifier (422) — neither offer_id nor merchant_internal_id was sent; no_fields_to_update (422); offer_not_found (404) — no such offer, or it belongs to another merchant; merchant_internal_id_not_unique (409) — the SKU matches several offers, and the body carries count. Send merchant_point_id to disambiguate.

Product and offer information

GET /product/{id}/info

Use the offer ID as {id}, not a product ID. The offer must belong to the authenticated merchant.

{
  "product": {
    "id": "<OFFER_PRODUCT_ID>",
    "root_product_id": "<ROOT_PRODUCT_ID>",
    "type": "variant",
    "status": "active",
    "name": {"ro": "Nume", "en": "Name", "ru": "Название"},
    "slug": {"ro": "nume", "en": "name", "ru": "nazvanie"},
    "url": "https://ivo.md/nume/p",
    "urls": {"ro": "https://ivo.md/nume/p", "en": "https://ivo.md/en/name/p", "ru": "https://ivo.md/ru/nazvanie/p"}
  },
  "offer": {
    "id": "<OFFER_ID>",
    "price": 1999,
    "currency": "MDL",
    "availability": 5,
    "availability_on_order": 0
  },
  "history": [
    {
      "price": 2099,
      "currency": "MDL",
      "availability": 3,
      "availability_on_order": 0,
      "changed_at": "2026-08-20T11:04:35+03:00",
      "source": "merchant_api"
    }
  ],
  "status": "success"
}
Output fieldTypeMeaning
product.idstringThe product the offer is attached to. For a product with variants this is the variant, not the parent.
product.root_product_idstringThe main (parent) product. Several variants share it, and the public page belongs to it.
product.typestringmain or variant.
product.statusstringThe catalogue status of the offer's product, e.g. active or draft.
product.nameobjectThe catalogue name per language (ro, en, ru). IVO writes it, so it can differ from the name you sent.
product.slugobjectThe URL segment per language.
product.urlstringThe Romanian public URL — the shortest link to give a buyer.
product.urlsobjectThe public URL per language.
offer.id, offer.price, offer.currency, offer.availability, offer.availability_on_order—The offer's current values, as IVO stores them right now.
history[]arrayUp to 100 price/stock changes, most recent first. Each entry carries price, currency, availability, availability_on_order, changed_at (ISO 8601) and source — which system made the change (merchant_api, api_sync, and so on).

Errors: offer_not_found (404); product_not_accessible (403) — the offer belongs to another merchant; product_not_found (404); pending_approval (202) — the main product is not published yet, and the body carries product_id and product_status; product_slug_missing (422).

POST /product/info-by-sku

The same detailed answer, found by your own SKU instead of an IVO ID. Use it when you do not store IVO identifiers on your side.

{
  "import_name": "daily-sync",
  "merchant_internal_id": "PRODUCT-123"
}
Input fieldTypeMeaning
merchant_internal_idstring, requiredYour SKU. The lookup searches the imported rows of your merchant account.
import_namestring, requiredValidated as required, but the lookup does not filter by it: a SKU is searched across all of your imports. Send the same name you used on /product/sync.

On success the body is identical to GET /product/{id}/info. While the product is still travelling through the pipeline, the answer is an error envelope:

messageHTTPMeaning
not_imported404No imported row carries this SKU. Send the product with /product/sync first.
queued202The row is waiting to be processed. Poll again.
processing202The row is being processed right now. Poll again.
pending_approval202The product exists but is not published yet. Nothing to do on your side.
the row's error code500Processing failed. The body carries import_row_id, import_id, ai_product_id, ai_error and error.
the row's offer status404The row completed without producing an offer (for example unauthorized_category).
offer_deleted404The row points at an offer that no longer exists.

Create or synchronize one product

POST /product/sync

Creates a product or updates an existing offer. Reusing the same import_name with a stable merchant_internal_id updates the same row instead of creating duplicates, so this endpoint is safe to call repeatedly for the same catalogue.

{
  "mode": "async",
  "import_name": "daily-sync",
  "merchant_internal_id": "PRODUCT-123",
  "name": "Example Phone 256 GB Black",
  "price": 1999,
  "currency": "MDL",
  "availability": 5,
  "availability_on_order": 0,
  "merchant_point_id": "<POINT_ID>",
  "brand": "Example",
  "ean": "5940000000000",
  "description": "Product description",
  "packages": [
    {"weight": 1200, "dimensions": "30x20x10 cm"}
  ],
  "images": ["https://example.com/product.jpg"]
}

packages lists the boxes the product ships in, one entry per box, each with its own weight and size; IVO counts the boxes from the list. The example above is a product in one box. A bed that ships in three boxes has three entries:

{
  "import_name": "daily-sync",
  "merchant_internal_id": "BED-160",
  "name": "Example Bed 160x200 Oak",
  "price": 8999,
  "currency": "MDL",
  "availability": 2,
  "merchant_point_id": "<POINT_ID>",
  "packages": [
    {"weight": 25000, "dimensions": "200x90x12 cm"},
    {"weight": 25000, "dimensions": "200x90x12 cm"},
    {"weight": 12000, "dimensions": "120x20x10 cm"}
  ],
  "images": ["https://example.com/bed.jpg"]
}

The bed ships in 3 boxes (62 kg in total) under one tracking number; delivery is priced on the total weight and volume. The older weight and dimensions fields still work for a product in one box, but they are deprecated: send packages.

Input fieldTypeMeaning
pricenumber, requiredSelling price, greater than 0 (minimum 0.01). Must be a plain number — 1999 or 1999.00, not "1 999,00".
retail_pricenumber or stringRecommended retail price (PRP), shown to buyers crossed out above your price. In the same currency as price, or with its own ("2499 MDL"). Send it only when it is above your selling price — the manufacturer's price, or your regular price while a discount is on — and leave it out otherwise. Your price modifier is not applied to it.
namestring, required without product_idThe full product name. IVO matches it against the catalogue and creates the product from it, so include the brand, model and the distinguishing attributes (capacity, colour). A name that is too vague is rejected with the row error too_generic.
product_idstringAn existing IVO product ID. Bypasses catalogue matching completely and only writes the offer — the fastest path when you already know the product. If that product already has variants, you must also send offer_product_id.
offer_product_idstringThe variant of product_id the offer attaches to. Required when the main product has variants, because IVO never guesses which variant you are stocking.
merchant_internal_idstringYour own stable product identifier (SKU). It is the key that links your catalogue to IVO on every later call. Strongly recommended: without it, rows are matched by a hash of the whole payload.
legacy_merchant_internal_id / legacy_merchant_internal_idsstring / arrayIdentifiers this product used to be sent under. Use them when your SKU scheme changes, so the existing row and offer are reused instead of a duplicate product being created.
import_namestringThe name of the import list this product belongs to, and the key you poll status with. It is slugified: spaces and punctuation become _ or are dropped, and it is cut to 64 characters ("My Daily Sync" → my_daily_sync). Empty means default. Use one stable name per source.
merchant_point_id / merchant_pointstringThe point ID, or the exact point name. If the account has a single point, it is selected automatically.
availabilityinteger or stringImmediate stock. Text is parsed leniently: "10 pcs" → 10, "2-3" → 3, "in stock"/"da" → 1, "out of stock" and anything unrecognised → 0. If neither availability nor availability_on_order is sent, immediate stock defaults to 1.
availability_on_orderinteger or stringStock available on order, parsed the same way.
currencystringISO 4217 code, MDL by default.
unit (or order_unit)stringUnit of sale: what one price buys. Your ERP's code or a word: EA/pcs — piece (the default; nothing is shown); MTR/m — metre; MTK/m2 — square metre; MTQ/m3 — cubic metre; KGM/kg — kilogram; LTR/l — litre; PA — pack; SET — set; PR — pair; RO — roll. Buyers see the price as per metre, per pack and so on, and the quantity in the cart counts this unit.
conditionstringnew (default), used or refurbished. One new offer exists per product and point; non-new offers may be several.
qualitystringFree-text grade that tells apart several non-new offers of the same product at the same point.
brandstringBrand name. Helps matching and is used when the product has to be created.
ean, asin, janstringGlobal product barcodes/identifiers. The strongest matching signal there is — send them when you have them.
descriptionstringProduct description used for matching and, for a new product, as source material.
images (or image)array or stringImage URLs, first one first. IVO downloads them; they must be publicly reachable.
packagesarrayThe boxes the product ships in, one entry per box; the number of boxes is the number of entries. Each entry has weight, the packed weight of that box in grams (1200, or with a unit: "1.2 kg", "2.6 lbs"), and its outer size as dimensions ("30x20x10 cm") or length, width, height, in cm unless you write another unit ("1200 mm", "1.2 m"). A product in one box sends a single entry. All boxes travel under one tracking number.
weight, dimensions, length, width, height, volume, package_countdeprecatedThe older one-box form, replaced by packages. Read only when packages is absent: they describe one box (or package_count identical boxes), in the same units as above; volume only with its unit ("45 l").
item_group_id, item_group_title, variant_option, variant_optionsstring / objectVariant grouping hints: products sharing an item_group_id are treated as variants of one parent, and the option values say what distinguishes them (colour, size).
force_reprocessbooleantrue restarts the row from zero, dropping previous matching results. Use it to recover a row that was matched wrongly — not on every sync, because it re-runs the whole matching pipeline.
modestringasync (default) returns immediately. sync holds the connection until the row finishes, up to a server-side limit (300 seconds by default), and returns anyway if the work is still running.
promotionsarrayThe offer's promotional prices. Applied only when the offer already exists. See "Promotional prices" below.

Any other field is stored with the row but is only understood if its name matches an import column.

Boxes. The weight and boxes you send take priority: whenever they change, they replace IVO's estimate or an administrator's correction. Whatever you don't send — the size, for example — is estimated automatically. Delivery is priced on the total weight and volume of the boxes.

Recommended retail price. The retail_price you send replaces IVO's estimate, which is then never requested for that product. Whenever it changes it also replaces an administrator's correction. A product sold by several merchants keeps the price of the first merchant who sent one.

Unit of sale. unit belongs to the product, not the offer: a product is sold in the same unit whoever sells it. Send the unit it is ordered in — the one the price buys — never the content unit: a pack of 2 chairs is PA, not EA. A sync without unit leaves the unit unchanged, and EA sets it back to a piece. A product sold by several merchants keeps the unit of the first merchant who sent one.

Responses

All three are HTTP 200 with "status": "success". Which fields are present tells you what happened.

Fields returnedWhat happened
sync_status: "completed", offer_id, offer_product_id, message: "Offer saved (AI bypassed)"You sent product_id: the offer was written directly, with no matching and no approval step.
sync_status: "completed", offer_id, offer_product_id, import_id, row_id, and matched_by when a legacy identifier was usedAn existing offer with this merchant_internal_id was found and updated immediately. This is the normal answer for a product already in the catalogue.
sync_status: "processing", import_id, message: "Product creation started"A new — or materially renamed — product entered the import and approval flow. Poll /product-import/status or /product/info-by-sku for the outcome.
Output fieldTypeMeaning
sync_statusstringcompleted — the offer exists and nothing is pending. processing — the product was queued and you must poll for the outcome. This is the field to branch on, not status.
offer_idstringThe created or updated offer. Store it: it makes later price/stock updates a single fast call.
offer_product_idstringThe IVO product (variant) the offer hangs on.
import_idstringThe import list this product was filed under — the one named by import_name.
row_idstringThe import row for this product.
matched_bystringmerchant_internal_id, or legacy_merchant_internal_id when the offer was found through one of the old identifiers you supplied.
row_statusstringOnly in mode: sync when the wait expired: the row's state at that moment.

Errors: invalid_product_id (422), product_not_found (404), offer_product_not_found (404), offer_product_invalid (422) — the variant does not belong to that main product, missing_offer_product_id (422), missing_merchant_point (422) — no point could be resolved on the product_id path, invalid_price (422), unauthorized_category (403) — your account is not authorized to sell in that category, integration_not_found / integration_disabled (403) — the import_name names an integration that was deleted or switched off, and processing_failed (422) in mode: sync with error and import_id.

Synchronize multiple products

POST /product/sync-multiple

Between 1 and 100 products per request. Each entry of products takes exactly the fields of /product/sync, plus the extras below. This is the endpoint to build a full catalogue sync on.

{
  "mode": "async",
  "import_name": "daily-sync",
  "image_base_url": "https://shop.example.com/images",
  "products": [
    {
      "merchant_internal_id": "PRODUCT-123",
      "name": "Example Phone 256 GB Black",
      "price": 1999,
      "availability_by_point": {
        "<POINT_ID_1>": 5,
        "<POINT_ID_2>": 2
      }
    }
  ]
}
Batch fieldTypeMeaning
productsarray, required1 to 100 product objects. Split a larger catalogue into consecutive requests with the same import_name.
import_namestringSame rules as on /product/sync, and the same list: the two endpoints share an import when the name matches.
modestringasync (default) or sync.
image_base_urlstring (URL)Prefix prepended to relative image paths, so a feed can send /catalog/x.jpg. Stored on the import and reused on retries.
image_split_spacesbooleanTreat spaces inside an image value as separators between several paths.
image_split_commasbooleanTreat commas inside an image value as separators between several paths.
integration_run_idstring, max 64Marks every row of one full catalogue run. It lets IVO measure progress against the run's real total instead of the size of one batch, and lets a new run discard stock left by the previous configuration.
Per-product extraTypeMeaning
availability_by_pointobjectStock per point: {"<POINT_ID>": 5}. Send the complete picture for that SKU in one entry — it replaces the previous snapshot. The product then lands on a single import row instead of one row per point.
availability_onlybooleantrue makes the entry a pure stock update: only merchant_internal_id and availability are required, name and price are not. Unknown identifiers are skipped instead of creating products — this is a refresh stream, not a product source.
force_reprocessbooleanSkips the fast existing-offer path and restarts the row's matching.

Response: sync_status (completed when every product was already up to date, processing when rows were queued) and import_id, plus message when work was queued. Nothing is returned per product — read individual outcomes from /product-import/status or /product/info-by-sku. A payload that fails validation is rejected as a whole with validation_failed (422) plus the index of the offending product and its errors; also invalid_product_payload (422) and products_required (422).

Upload a product file

POST /product-import/upload-and-import

Sends a whole catalogue file in one request, exactly like uploading it in the merchant panel. Use multipart/form-data.

Input fieldTypeMeaning
filefile, requiredThe catalogue file. Accepted extensions: XLSX, XLS, CSV, TSV, ODS, JSON and XML.
modestringasync (default) returns as soon as the file is queued. sync waits for the import to finish, up to 300 seconds by default.
import_namestringOptional name to file this upload under, slugified like everywhere else. Pass one when you intend to poll the import: GET /product-import/status then finds it by that name. Without it the import keeps the uploaded file's own name, and you poll by import_id (or by that exact file name).
columnsarrayColumn mapping, one entry per column of the file in order. Use the canonical names: name, description, brand, ean, asin, jan, weight, volume, length, width, height, dimensions, package_count, packages, image, availability, availability_on_order, price, price_and_currency, retail_price, currency, unit, merchant_internal_id, and ignore for columns to skip. Map several columns to image to import several images. When columns is omitted, IVO detects the mapping from the header row.
Output fieldTypeMeaning
import_idstringThe created import. Keep it: it is what you poll the status endpoint with.
import_namestringThe name the import was filed under — your import_name after slugification, or the uploaded file's name. Poll with exactly this value.
import_statusstringThe import's real state: pending, ready_to_process, processing, completed, completed_with_errors or error.
messagestringImport started successfully, or a note that the work is still running in mode: sync.
importobjectOnly in mode: sync when the import reached a final state: the full import record.

For recurring synchronization prefer /product/sync-multiple: it updates the same rows in place, reports per-product outcomes, and needs no file.

Check import status

GET /product-import/status

Reports the progress of any import you created — through /product/sync, /product/sync-multiple or /product-import/upload-and-import — and the outcome for one product in it.

GET /product-import/status?import_name=daily-sync&merchant_internal_id=PRODUCT-123
GET /product-import/status?import_id=<IMPORT_ID>
Input fieldTypeMeaning
import_idstringThe import to report on, as returned by any sync or upload call. Send this or import_name; sending neither fails with missing_identifier (422).
import_namestringThe import list name. Matched both slugified (as sync files it) and exactly as written (as an upload files it), so the name you were handed back always resolves. Must belong to your merchant account.
merchant_internal_idstringWhich product's row to report. Can be omitted only when the import holds exactly one row; on a multi-row import, omitting it fails with merchant_internal_id_required (422).
{
  "import_id": "<IMPORT_ID>",
  "import_name": "daily-sync",
  "import_status": "processing",
  "error_type": null,
  "processed_rows": 87,
  "total_rows": 120,
  "row_count": 120,
  "row": {
    "id": "<ROW_ID>",
    "row_number": 12,
    "status": "completed",
    "error": null,
    "merchant_internal_id": "PRODUCT-123",
    "ai_product_id": "<AI_PRODUCT_ID>",
    "offer_product_id": "<VARIANT_ID>",
    "product_id": "<PRODUCT_ID>",
    "offer_id": "<OFFER_ID>",
    "product": {
      "id": "<VARIANT_ID>",
      "root_product_id": "<ROOT_PRODUCT_ID>",
      "type": "variant",
      "root_type": "main",
      "status": "active",
      "name": {"ro": "Nume", "en": "Name", "ru": "Название"},
      "slug": {"ro": "nume", "en": "name", "ru": "nazvanie"},
      "url": "https://ivo.md/nume/p",
      "urls": {"ro": "https://ivo.md/nume/p"},
      "has_public_url": true
    },
    "started_at": "2026-08-26T09:12:00+03:00",
    "ended_at": "2026-08-26T09:12:41+03:00"
  },
  "status": "success"
}
Output fieldTypeMeaning
import_idstringThe import this request resolves to.
import_namestringThe name it is filed under.
import_statusstringThe state of the whole import: pending, ready_to_process, processing, completed, completed_with_errors or error. This is the import's state — the top-level status only says the request itself succeeded.
processed_rows / total_rowsintegerProgress of the whole import: rows that reached completed or error, out of the expected total. Equal values mean the import is finished.
row_countintegerHow many rows the import currently holds.
error_typestring, nullSet when the import itself failed (an unreadable file, for instance), rather than an individual row.
row.statusstringThe state of the selected product: ready (queued), processing, completed, or error. This is the field to poll for one SKU.
row.errorstring, nullWhy this row failed, as a machine-readable code — see the row-level codes below.
row.merchant_internal_idstring, nullYour SKU as stored on the row.
row.ai_product_idstring, nullThe intermediate record produced while the product was being built. Useful only for support requests.
row.product_id / row.offer_product_idstring, nullThe matched product, and the variant the offer attaches to.
row.offer_idstring, nullThe resulting offer. Once it is present, later updates can go through /product-offer/update.
row.productobject, nullPublic product information: name and slug per language, url and urls, catalogue status, and has_public_url — false while the product has no live page yet.
row.started_at / row.ended_atdatetime, nullWhen processing of this row began and ended.
statusstringAlways success — it reports that the request itself succeeded. The import's own state is import_status.

Errors: missing_identifier (422) — neither import_id nor import_name was sent, import_not_found (404), import_row_not_found (404), merchant_internal_id_required (422).

Promotional prices (Black Friday, Cyber Monday)

During a promotion an offer can sell at a lower price of your choice, kept apart from its normal price. Each promotion has its own price, so the same product can have one price for Black Friday and another for Cyber Monday.

GET /promotions

Returns the promotions you can set prices for now: those scheduled and those running. Call it regularly to learn when a new promotion appears and which code to send its prices under. A promotion that is not in the list isn't taking prices at the moment.

Input: none beyond the API key header. name and merchant_info come in English; add ?language=ro or ?language=ru to get them in Romanian or Russian.

{
  "promotions": [
    {
      "code": "black-friday-2026",
      "name": "Black Friday",
      "starts_at": "2026-11-27T00:00:00+02:00",
      "ends_at": "2026-11-28T00:00:00+02:00",
      "state": "scheduled",
      "merchant_info": "Black Friday on IVO: Friday 27 November."
    }
  ],
  "status": "success"
}
Output fieldTypeMeaning
promotions[].codestringThe code to send prices under in promotions.
promotions[].namestringThe promotion's name.
promotions[].starts_at, promotions[].ends_atstringWhen the promotion starts and ends, in ISO 8601. The promotional price applies between these two moments.
promotions[].statestringscheduled — still to start; live — running now.
promotions[].merchant_infostringThe information for merchants about this promotion. Can be empty.

The same promotions appear in your Merchant Dashboard. You can also set prices without the API: per product, on its offers page in the dashboard, or through the price and quantity columns in your feed's Field mapping step.

Send a promotional price

Add promotions to a POST /product-offer/update call. It can be sent on its own, without price or stock:

{
  "merchant_internal_id": "PRODUCT-123",
  "merchant_point_id": "<POINT_ID>",
  "promotions": [
    {"code": "black-friday-2026", "price": 1599, "quantity": 10},
    {"code": "cyber-monday-2026", "price": 1699}
  ]
}
FieldTypeMeaning
codestringThe promotion's code, from GET /promotions.
pricenumber, nullThe promotional price, in the offer's currency. It must be lower than the offer's normal price. null, 0 or leaving the field out takes the offer out of that promotion.
quantityinteger, nullHow many units sell at this price; once they are gone, the rest sell at the normal price. A whole number, at least 1. Absent or null: every unit in stock.

The answer holds offer.promotions, the offer's prices for the promotions taking prices now, and promotion_errors, the items refused. A refused item does not fail the call: the answer stays HTTP 200 and the rest of the call is saved. A shortened answer:

{
  "offer": {
    "id": "<OFFER_ID>",
    "price": 1999,
    "promotions": [
      {
        "code": "black-friday-2026",
        "price": 1599,
        "currency": "MDL",
        "quantity": 10,
        "sold": 0
      }
    ]
  },
  "promotion_errors": [
    {"code": "cyber-monday-2026", "error": "promotion_closed"}
  ],
  "status": "success"
}
ErrorMeaning
promotion_not_foundNo published promotion has this code, or the promotion has ended.
promotion_closedThe promotion exists but isn't taking prices at the moment.
promotion_price_not_lowerThe price is not below the offer's normal price.
promotion_quantity_invalidquantity is not a whole number of at least 1.

What happens to the prices

  • The offer sells at the promotional price only during the promotion. Before and after it, it sells at its normal price.
  • It also sells at the normal price once the quantity units have sold, or if the normal price is no longer above the promotional one.
  • Updating price, stock or any other field never changes a promotional price. Only the promotions you name in promotions are touched.
  • Once a promotion stops taking prices, those already set stay and sell during the promotion, but they can no longer be changed or removed.

With /product/sync and /product/sync-multiple

Both accept the same promotions field, but apply it only when the call finds an existing offer. A new product has no offer yet, so send its prices once the offer exists. A call that sends product_id ignores the field. These endpoints do not report refused items: to see the answer, use POST /product-offer/update.

When an offer becomes visible

A successful call is not the same as a live product. An offer appears on ivo.md only when all of the following hold:

  • the merchant account is active;
  • its merchant point is approved and is_active;
  • the product has passed IVO's approval and has a public page;
  • price is present and greater than zero;
  • availability or availability_on_order is greater than zero.

If a synced product does not appear, check these five in order before opening a support request.

Common error codes

CodeHTTPMeaning
api_key_missing403No X-API-Key and no Authorization: Bearer header.
api_key_invalid403Unknown or revoked key.
merchant_not_found403The key is valid but its merchant account no longer exists.
integration_disabled / integration_not_found403The import_name names an integration that the merchant switched off or deleted.
unauthorized_category403Your account is not authorized to sell in that category.
product_not_accessible403The offer belongs to another merchant.
offer_not_found, product_not_found, offer_product_not_found, point_not_found, import_not_found, import_row_not_found, not_imported, offer_deleted404The named record does not exist, or does not belong to your account.
merchant_internal_id_not_unique409The SKU matches several offers. Add merchant_point_id.
missing_identifier, no_fields_to_update, invalid_price, invalid_product_id, missing_merchant_point, missing_offer_product_id, offer_product_invalid, merchant_internal_id_required, products_required, invalid_product_payload, validation_failed, product_slug_missing, processing_failed422The payload is incomplete or inconsistent. validation_failed carries index and errors.
queued, processing, pending_approval202Not an error: the answer is not ready yet. Poll again.

Row-level import errors appear in row.error rather than as an HTTP status: too_generic (the name does not identify a specific product), missing_name_value, missing_category, unauthorized_category, variant_not_found, duplicate, no_availability, and missing_merchant_point.

Promotional price errors appear in promotion_errors rather than as an HTTP status. See "Promotional prices" above.

Was this article helpful?

Share quick feedback so we can improve it.

Related articles

Merchant Integration

Extensions: how they work

A generic overview of how IVO marketplace extensions typically install, authorize, sync, and report status.

Merchant Integration

Magento 2 extension: installation

Install the IVO Marketplace Magento 2 extension and connect your store to IVO.

Merchant Integration

OpenCart 4 extension: installation

Install the IVO Marketplace OpenCart extension and connect your store to IVO.

Merchant Integration

PrestaShop module: installation

Install the IVO Marketplace PrestaShop module and connect your store to IVO.