API Elbuz

29 хв

API дає зовнішній програмі доступ до даних вашого акаунта: товари, прайс-листи, контрагенти, документи, довідники. Це стаття-довідник: усі приклади нижче виконані на живому акаунті, і відповіді в них справжні.

Стаття розрахована на розробника. Адміністратору достатньо першого розділу: завести ключ і передати його тому, хто пише інтеграцію.

Крок 1. Ключ доступу

Вікно
Налаштування системи → розділ «Інші налаштування» → плитка «API та токени доступу»
Що створюєте
пару «користувач + ключ доступу» для однієї зовнішньої програми
Скільки
окремий ключ на кожну інтеграцію
Розділ «Інші налаштування» з плиткою «API та токени доступу»
1Плитка «API та токени доступу»
2Розділ «Інші налаштування»
  1. Плитка «API та токени доступу» — звідси відкривається список ключів API.
  2. Розділ «Інші налаштування» — сюди зібрані вікна, яких немає в головному меню.
Поле картки ключаПараметр у запитіЩо робить
«Користувач»usernameімʼя для входу
«Ключ доступу»keyсекрет; кнопка «Генерувати» створює його за вас
«Активність»—вимкнений ключ не пускає ні на вхід, ні за вже виданим токеном
«Тільки для читання»—будь-яка зміна даних відповідає помилкою; читання працює
«IP-адреси для доступу»—перелік через кому, пробіл або з нового рядка. Порожньо — звідусіль
«Дата закінчення доступу»—ключ працює по цю дату включно; порожня дата — безстроково
«Примітка»—кому й навіщо виданий ключ

Три поля закривають доступ миттєво, а не з наступного входу. «Активність», «Дата закінчення доступу» й «Тільки для читання» перечитуються з картки ключа на кожному запиті, тому діють і на вже виданий токен: вимкнули ключ або настав наступний день після дати — поточний токен перестає працювати без перелогіну. Це зручно, коли ключ треба відкликати негайно.

Крок 2. Швидкий старт

Мінімальний робочий скрипт: вхід, запит даних, друк результату. Підставте свою адресу, імʼя користувача та ключ.

<?php
$host = 'https://ваш-акаунт.elbuz.com';   // адреса вашого акаунта
$user = 'site';                           // «Користувач» з картки ключа
$key  = 'ВАШ_КЛЮЧ';                       // «Ключ доступу»

function elbuz_call($host, $route, array $post) {
    $ch = curl_init($host . '/admin/index.php?route=' . $route);
    curl_setopt_array($ch, array(
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query($post),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 120,
    ));
    $body = curl_exec($ch);
    return json_decode($body, true);
}

// 1. вхід — отримуємо токен
$login = elbuz_call($host, 'api/login', array('username' => $user, 'key' => $key));
if (empty($login['api_token'])) {
    exit('Не вийшло увійти: ' . json_encode($login, JSON_UNESCAPED_UNICODE));
}
$token = $login['api_token'];

// 2. запит даних
$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'   => $token,
    'scope'       => 'category',
    'max_rows'    => 2,
    'limit_field' => 'name',
));

print_r($res);

Відповідь на вхід:

{
    "success": "Сеанс API успішно розпочато!",
    "api_token": "b439dd5c3b386d36464bef450d"
}

Відповідь на запит категорій — дослівно те, що повернув сервер:

{
    "item_list": {
        "83": {
            "category_id": "83",
            "parent_id": "11",
            "name": "Варочные поверхности"
        },
        "84": {
            "category_id": "84",
            "parent_id": "11",
            "name": "Встраиваемые вытяжки"
        }
    },
    "meta": {
        "item_scope": "category",
        "total_row": 234,
        "page_total": 117,
        "page_next": 2,
        "page_current": 1,
        "request_time": "00:00:00"
    }
}

Зверніть увагу: у запиті було limit_field=name, а у відповіді ще й category_id із parent_id. Так і має бути — обовʼязкові поля довідника дописуються завжди.

Основи протоколу

ЩоЯк
Базова адресаадреса акаунта + /admin/index.php?route= + маршрут
Методтільки POST
Тіло запитузвичайна форма, application/x-www-form-urlencoded
ВідповідьJSON із заголовком Content-Type: application/json
Код HTTPзавжди 200 — навіть на помилку
Токенпараметр api_token у тілі POST або в адресі

Не орієнтуйтеся на код HTTP: помилка теж приходить із кодом 200. Розбирайте структуру: помилка — це JSON-масив рядків, успішна відповідь — обʼєкт із item_list або meta.

Час життя токена

Токен — це ідентифікатор сеансу. Сеанси, до яких не зверталися більше години, видаляються; кожен запит із токеном продовжує сеанс. Тому постійно працююча інтеграція входить один раз, а новий api/login робить лише у відповідь на помилку доступу.

function elbuz_get($host, $user, $key, &$token, array $post) {
    $post['api_token'] = $token;
    $res = elbuz_call($host, 'api/scope/get', $post);

    // помилка приходить масивом рядків — можливо, сеанс закінчився
    if (isset($res[0]) && is_string($res[0])) {
        $login = elbuz_call($host, 'api/login', array('username' => $user, 'key' => $key));
        if (empty($login['api_token'])) return $res;   // справа не в сеансі
        $token = $login['api_token'];
        $post['api_token'] = $token;
        $res = elbuz_call($host, 'api/scope/get', $post);
    }
    return $res;
}

Обмеження частоти

У ключа є межа звернень — за замовчуванням 30 за хвилину. Влаштована вона як мінімальний проміжок: 60 поділити на це число, тобто за замовчуванням не частіше ніж раз на дві секунди. Накопичити паузу й видати пачку запитів поспіль не можна — відхилений буде кожен, що прийшов раніше за проміжок.

{"Error": "Rate limit exceeded! Time passed:0.0582 Time minimal request:0.1"}

У відповіді видно обидва числа: скільки минуло від минулого запиту й скільки потрібно. Формат тут інший, ніж у решти помилок, — ключ Error з великої літери й обʼєкт замість масиву. Обробляйте обидва варіанти:

if (isset($res['Error'])) {                    // межа частоти
    sleep(2);
    // повторити той самий запит
}
if (isset($res[0]) && is_string($res[0])) {    // звичайна помилка API
    throw new RuntimeException($res[0]);
}

З інтерфейсу межа не міняється — це питання до технічної підтримки.

НіТакНіТакНіТакНіТакТакНіЗапит з api_tokenСеанс живий, у ключа«Активність»і дата доступу не минула?«немає дозволу на доступдо API»Минув мінімальнийпроміжоквід минулого запиту?Rate limit exceededIP-адреса є в спискуключаабо список порожній?Це add, update чи delete?Запит виконуєтьсяУ ключа стоїть«Тільки для читання»?«немає дозволу на змінуданих»
Перевірки, які проходить кожен запит із токеном, по черзі. Межа частоти перевіряється раніше за IP-адресу, а «Тільки для читання» — лише на операціях, що змінюють дані

Мова відповідей

Тексти приходять мовою співробітника, від імені якого працює ключ — це власник акаунта. А помилка доступу без токена приходить мовою бази за замовчуванням, бо співробітник ще не визначений. Тобто два повідомлення одного API можуть бути різними мовами. Ще один аргумент розбирати відповідь за структурою, а не за текстом.

Довідник операцій

МаршрутПризначенняОбовʼязкові параметри
api/loginотримати токенusername, key
api/scope/describeопис полів довідникаapi_token, scope
api/scope/getотримати записиapi_token, scope
api/scope/addдодати запис або пачкуapi_token, scope + поля запису
api/scope/updateоновитиapi_token, scope, ідентифікатор
api/scope/deleteвидалитиapi_token, scope, ідентифікатор
api/scope/searchпошук товарів пошуковим індексомapi_token, scope=product

Довідник задають параметром scope або дописують у маршрут — обидва варіанти рівнозначні:

api/scope/get          + scope=currency
api/scope/get/currency

Довідники за операціями

Набори різні. Це перше, що варто звірити перед проєктуванням інтеграції: читати можна майже все, змінювати — далеко не все.

ОпераціяКоди довідників
getcategory, product, product_price, attribute, attribute_block, language, article, manufacturer, stock_status, warehouse, store, company, contractor, contractor_group, currency, document, document_type, document_status, document_payment_status, document_delivery_status, print_template
addcategory, product, product_price, manufacturer, contractor, document, document_extended, user, company, attribute, attribute_block, article, contractor_price, mail_send
updatecategory, product, currency, contractor, document
deletecategory, product, contractor, attribute_block, attribute, manufacturer, article, document
describeproduct, product_price, manufacturer, contractor, document, user, company
searchproduct

product_price є в додаванні, але немає в оновленні: рядки прайсу через API створюються, а змінюються тільки завантаженням прайс-листа. Плануйте інтеграцію з урахуванням цього.

describe: звідки брати назви полів

Назви полів у запитах — не ті, що підписані в інтерфейсі. describe повертає реєстр полів: внутрішню назву, людський підпис і тип.

$res = elbuz_call($host, 'api/scope/describe', array(
    'api_token'   => $token,
    'scope'       => 'contractor',
    'limit_field' => 'datafield,datafield_type,grid_field_name',
));
{
    "item_list": {
        "347": {"row_id": "347", "datafield": "name",         "datafield_type": "string", "grid_field_name": "Назва"},
        "349": {"row_id": "349", "datafield": "phones",       "datafield_type": "string", "grid_field_name": "Телефон"},
        "560": {"row_id": "560", "datafield": "status_name",  "datafield_type": "string", "grid_field_name": "Статус"},
        "562": {"row_id": "562", "datafield": "manager_name", "datafield_type": "string", "grid_field_name": "Відповідальний менеджер"}
    },
    "meta": {"item_scope": "contractor", "total_row": 118, "page_total": 1, "page_next": 0, "page_current": 1, "request_time": "00:00:00"}
}

Це той самий реєстр, який видно у вікні «Налаштувати сітку» з перемикачем «Тех. назви». Без limit_field кожне поле приходить із трьома десятками службових властивостей — беріть три потрібні колонки.

get: вибірка даних

Параметри

ПараметрЗа замовчуваннямЩо робить
scope—код довідника, обовʼязковий
page1номер сторінки
max_rows100записів на сторінці. Стеля 500 у хмарі, 10 000 в офлайн-версії; більше просто обрізається
language_idмова акаунтамова текстових полів
limit_fieldусі поляперелік потрібних полів через кому
extend—вкладені дані: фото, характеристики, позиції документа
filterscount і далі—умови відбору

Обовʼязкові поля

Навіть якщо ви їх не просили, ці поля приходять завжди — без них відповідь неможливо зіставити з вашими даними:

ДовідникДописуються завжди
productproduct_id, name, price
product_pricesupply_product_id, contractor_id, price_id, product_id, sku_manufacturer, manufacturer, name, price, stock_status_id, field_ssd_stock_status_name
categorycategory_id, parent_id, name
documentdocument_id, contractor_id, type_id, document_type_name, document_number
attributeattribute_id, attribute_name
languagelanguage_id, code, name
print_templateнічого не дописується
рештавласний ідентифікатор і name

Сторінки

Записи приходять обʼєктом, а не масивом: ключ — ідентифікатор запису. У блоці meta лежить усе для обходу, ознака кінця — page_next дорівнює нулю.

$page = 1;
$all  = array();

do {
    $res = elbuz_call($host, 'api/scope/get', array(
        'api_token'   => $token,
        'scope'       => 'product',
        'max_rows'    => 500,          // стеля для хмари
        'limit_field' => 'product_id,name,price,quantity',
        'page'        => $page,
    ));

    if (!isset($res['item_list'])) break;        // помилка або порожня вибірка

    $all += $res['item_list'];                   // ключі = ID, дублів не буде
    $page = (int)$res['meta']['page_next'];

    sleep(2);                                     // межа частоти за замовчуванням
} while ($page > 0);

echo 'отримано: ' . count($all);

На довіднику валют із тринадцяти записів по пʼять на сторінку цей цикл робить рівно три запити: третя сторінка повертає page_next: 0, і обхід завершується.

Порожня вибірка — це не порожній item_list, а помилка «Елемент не знайдено». Якщо фільтр нікого не пропустив, перевірка isset($res['item_list']) обірве цикл — саме тому вона стоїть до розбору.

Фільтри

Для product і document працює той самий відбір, що й у сітках інтерфейсу: filterscount — скільки умов, далі для кожної четвірка параметрів із номером, починаючи з нуля.

ПараметрЗначення
filterdatafield0поле, як у describe
filtervalue0значення
filtercondition0EQUAL, NOT_EQUAL, CONTAINS, STARTS_WITH, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL
filteroperator00 — зʼєднати з наступною умовою через «і», 1 — через «або»

Товари, змінені після заданої дати:

$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'        => $token,
    'scope'            => 'product',
    'max_rows'         => 2,
    'limit_field'      => 'product_id,name,quantity,date_modified',
    'filterscount'     => 1,
    'filterdatafield0' => 'date_modified',
    'filtervalue0'     => '2026-01-01',
    'filtercondition0' => 'GREATER_THAN_OR_EQUAL',
    'filteroperator0'  => 0,
));
{
    "item_list": {
        "6068": {
            "product_id": "6068",
            "price": "1200.00000000",
            "quantity": "0",
            "date_modified": "2026-09-12 14:54:23",
            "name": "Ardesto LESH-Y305"
        }
    },
    "meta": {
        "item_scope": "product",
        "total_row": 126999,
        "page_total": 63500,
        "page_next": 2,
        "page_current": 1,
        "request_time": "00:00:00"
    }
}

Фільтрувати можна й за характеристиками: імʼя поля — attribute_ плюс номер характеристики, його дає describe.

Вкладені дані

Фото, характеристики й позиції документа лежать не в самому записі — їх домовляють параметром extend через кому.

ДовідникЗначення extendПоле у відповіді
productattribute, image, file, video, categoryextend_attribute, extend_image, extend_file, extend_video, extend_category
product_priceattribute, imageті самі
documentcontractor, productextend_contractor, extend_product
$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'   => $token,
    'scope'       => 'product',
    'max_rows'    => 1,
    'limit_field' => 'product_id,name',
    'extend'      => 'image,category',
));
{
    "item_list": {
        "6068": {
            "product_id": "6068",
            "price": "1200.00000000",
            "name": "Ardesto LESH-Y305",
            "extend_image": [
                {
                    "product_image_id": "201587",
                    "image": "100/68/product_image_6068_201587.jpg",
                    "sort_order": "1",
                    "image_original": "https://…/product_image_6068_201587.jpg",
                    "image_cloud_url": ""
                }
            ],
            "extend_category": [ {"category_id": "1201"} ]
        }
    }
}

Набір полів вкладеного блока теж керований: extend_limit_field_attribute, extend_limit_field_image, extend_limit_field_product і подібні. За замовчуванням у характеристик це attribute_id, attribute_name, attribute_value, у позицій документа — product_id, sku, mpn, name, quantity, price, price_sum, ean, isbn.

З параметром extend форма відповіді змінюється: item_list перестає бути обʼєктом із ключами-ідентифікаторами й приходить звичайним списком. Код, написаний без extend, зламається рівно тоді, коли хтось його додасть. Розбирайте обидві форми одразу:

$rows = $res['item_list'];
$rows = isset($rows[0]) ? $rows : array_values($rows);   // однаково для обох форм
foreach ($rows as $row) { /* … */ }

add, update, delete

Відповідь у всіх трьох однакова й коротка:

{"meta": {"item_scope": "product", "result": 1024, "request_time": "00:00:00"}}

У result лежить те, що повернула операція: для додавання — ідентифікатор нового запису, для решти — ознака успіху. Окремого поля з текстом помилки валідації немає: якщо запис не створився, result буде порожнім.

Пакетні операції з товарами відповідають інакше: у result лежить обʼєкт. Після додавання в ньому status, total_item_add — скільки товарів створено, і item_list_add — список саме створених, з product_id і uuid кожного. Після оновлення — total_item_update. Якщо пакет відхилено цілком, status дорівнює 0, а в status_text причина: наприклад, «Невідоме поле» з назвою поля або «Максимальна кількість товарів» із межею. Звіряйте total_item_add з кількістю відправлених рядків: частину рядків пакет пропускає без помилки, про це — у розділі про обрив і повтор.

Пакетні операції

Замість полів одного запису передається item_list — список записів:

$res = elbuz_call($host, 'api/scope/add', array(
    'api_token' => $token,
    'scope'     => 'product',
    'item_list' => array(
        array('name' => 'Товар 1', 'price' => 100, 'sku' => 'A-001'),
        array('name' => 'Товар 2', 'price' => 200, 'sku' => 'A-002'),
    ),
));
ДовідникОпераціяЗаписів за раз
productдодавання й оновленнядо 10 000
product_priceдодаваннядо 100

Пакет — не просто зручність. З межею в один запит на дві секунди десять тисяч товарів поштучно — це майже шість годин, пакетом — один запит.

Позначка джерела

Параметр source_code в додаванні позначає, звідки прийшли дані; позначка записується в поле товару «Джерело: код», і потім видно, що товар завела саме ця інтеграція, а не людина. Без параметра ставиться jumper.

Пакетне оновлення api/scope/update свою позначку не бере з запиту: кожному оновленому товару воно записує в «Джерело: код» значення api. Тобто ваша позначка живе, доки товар не оновили пакетом, — враховуйте це, якщо збираєтеся шукати товари інтеграції за нею (відкат помилкового пакета).

Видалення

Ідентифікатор передається в полі з іменем довідника: category_id, product_id, manufacturer_id, attribute_id, attribute_block_id, article_id, document_id.

Передавайте лише те поле, що відповідає довіднику. Якщо потрібного ідентифікатора немає, запит не завершиться помилкою — обробка піде далі за списком довідників і може дійти до видалення документа, якщо в тілі запиту трапиться document_id. Відповідь при цьому назве той довідник, який ви просили. Не складайте універсальне тіло запиту з ідентифікаторами різних довідників.

Проведені документи не видаляються — така спроба повертає порожній результат.

Що відбувається з даними

Розділ для того, хто вже запускає інтеграцію на живому акаунті: що перепишеться поверх ручних правок, що буде після обриву, чия ціна переможе і як прибрати наслідки помилкового пакета.

Повторне оновлення поверх ручних правок

api/scope/update для товарів переписує рівно ті поля, які є в запиті, а решту не чіпає. Товар шукається тільки за product_id: рядок з ідентифікатором, якого в базі немає, пропускається без помилки й не входить у total_item_update.

Прапорці картки «Не оновлювати» і «Фіксована ціна» не зупиняють оновлення через API. Якщо менеджер поправив назву чи ціну руками й поставив прапорець, наступний update з цим полем перепише значення все одно. Прапорці захищають від оновлення каталогу з прайсів і від наценок, але не від прямого запису через API.

Окремо про вкладені списки в пакетному оновленні:

  • якщо в записі є image_list, усі фото товару видаляються й замінюються присланими; прапорець «Не оновлювати фото» цього не зупиняє. Немає image_list — фото лишаються як були;
  • якщо є attribute_list, характеристики товару стираються й записуються з пакета. Тут прапорці діють: у товару з «Не оновлювати» або «Не оновлювати атрибути» характеристики лишаються старі.

Поля, позначені в реєстрі полів як «тільки для читання», пакет пропускає без помилки. Поле, якого в реєстрі немає зовсім, відхиляє весь пакет з текстом «Невідоме поле» і його назвою. Перевіряються назви полів першого запису пакета.

Чим закріпити своє значення:

СитуаціяЩо робити
Поле веде людина в Elbuz, інтеграція його не має чіпатине передавати це поле в update. В одиночному оновленні (без item_list) значення -1 теж означає «поле не чіпати»
Потрібно дописувати дані, не затираючи заповненезамість update слати api/scope/add з параметром update_exist_product=1 — його правила нижче

Додавання з update_exist_product=1 знаходить наявні товари так само, як при захисті від дублів (нижче), і оновлює їх за іншими правилами, ніж update:

  • порожнє значення або нуль у пакеті не затирає заповнене поле. Виняток — поля, де нуль означає дію: quantity, status, sort_order, subtract_quantity, shipping і всі поля flag_*;
  • ціни (price, price_rrp, price_old, price_cost, price_special) не змінюються у товарів з «Фіксована ціна»;
  • товари з «Не оновлювати» пропускаються цілком.

Без update_exist_product додавання поля наявних товарів не оновлює: знаходить і пропускає. Лише привʼязки до категорій зі списку category_list наявним товарам дописуються. Замість прапорця можна передати field_list_update_exist_product — список полів, які оновити; тоді пишуться тільки вони, так само оминаючи товари з «Не оновлювати» й ціни товарів із «Фіксована ціна» (крім price_special — її цей режим не захищає). Назви, яких немає серед полів товару, пропускаються без помилки — решта списку оновлюється.

Обрив посеред пакета і повтор

Пакет на 10 000 товарів сервер обробляє як послідовність окремих записів у базу, без транзакції: спершу категорії й виробники, потім самі товари, потім категорії товарів, характеристики, фото, знижки. Якщо обробка обірвалася посередині, в базі лишається те, що встигло записатися.

Повтор того самого пакета товари не задвоює. Перед записом кожен рядок шукається серед наявних товарів:

  • якщо в рядку є uuid — за ним;
  • якщо uuid порожній — за збігом трьох полів разом: mpn, назва й ext_url (без урахування регістру й пробілів по краях).

Знайдений товар удруге не створюється. Але фото й характеристики при додаванні пишуться тільки новоствореним товарам. Якщо обрив стався після створення товарів, але до запису фото, повтор фото не додасть — товар уже вважається наявним. Характеристики, яких бракує, повтор допише, якщо передати update_existing_attr[product_values]=1: відсутні значення додаються, наявні не чіпаються. Фото доведеться дослати через api/scope/update з image_list.

Передавайте в кожному товарі власний стабільний uuid. Тоді повтор знаходить товар однозначно, навіть якщо ви між спробами змінили назву.

Без помилки пакет пропускає два види рядків, і їх видно лише з того, що total_item_add менший за відправлене:

  • рядок без назви — відкидається до запису;
  • новий товар без категорії — не створюється. Категорію пакет визначає за category_id, за повним шляхом у category_name_full (частини через /; якщо жодної частини шляху в базі немає, ланцюжок категорій створюється, інакше товар лягає в найглибшу знайдену за назвою) або за списком category_list. Не вийшло жодним способом — товар не записується. Параметр category_create_by_source=1 кладе такі товари в окрему службову категорію з кодом джерела в назві; вона створюється вимкненою й без вивантаження.
НіТакТакТакНіНіТакНіТакНіТакНіРядок пакетаapi/scope/addУ рядку є назва?Рядок пропускається безпомилки,у total_item_add невходитьТовар уже є?За uuid, а без uuid — заmpn,назвою й ext_url разомПереданоupdate_exist_product=1абоfield_list_update_exist_product?Поля оновлюються.Поля товарів з «Неоновлювати» незмінюютьсяПоля товару незмінюються.Дописуються лишекатегорії з category_listКатегорію знайдено заcategory_idабо category_name_full?Товар створюєтьсяразом із фото йхарактеристикамиУ category_list є головнакатегорія,знайдена в базі?category_create_by_source=1?Товар іде в службовукатегорію джерела
Доля одного рядка пакета. Ліва гілка — товар, що вже є: без update_exist_product його поля не змінюються. Права — новий товар: без назви або без категорії він не створюється і в total_item_add не потрапляє

Чия ціна залишиться

Ціну товару в Elbuz пишуть три механізми: API, наценки базового каталогу й оновлення каталогу з прайс-листів. Хто записав останнім, того значення й лишиться — з такими правилами:

Хто пишеЩо робить з ціноюЩо його зупиняє
api/scope/updateпише ціну як є, наценки після себе не запускаєнічого: «Фіксована ціна» й «Не оновлювати» не діють
api/scope/addпише вашу ціну, а потім у тому ж запиті проганяє наценки базового каталогу по щойно створених товарах: якщо є активні правила наценки й ви передали price_cost більше нуля, ціна перераховується із закупівельної«Фіксована ціна» або «Не оновлювати» в самому пакеті
оновлення каталогу з прайсів і наценкипереписують ціну товарів, зіставлених з прайсом«Фіксована ціна» (і «Не оновлювати» — для наценок)

Тобто ціна, яку ви передали через API, доживе лише до наступного оновлення каталогу чи прогону наценок, якщо в товару не стоїть «Фіксована ціна». Щоб ціна з API лишалася головною, передайте разом із нею flag_fix_price=1. Прапорець знімається сам після дати в полі «Дата для зняття прапора "Фіксована ціна"», якщо її задано, — це перевіряється на кожному оновленні каталогу.

Коли додане потрапить на сайт і у вивантаження

Самі операції API нічого не вивантажують: ні add, ні update не запускають тунель чи вивантаження файлу. Товар потрапить далі найближчим прогоном того вивантаження, яке у вас налаштоване, — вручну чи за розкладом.

У межах самого запиту додавання вже зроблено все, що треба каталогу: наценки, службові дані для сортування й зведена (плоска) таблиця товарів. Якщо в акаунті кілька активних магазинів, нові товари додаються до магазину з параметра store_id, а без нього — до магазину з номером 1.

Тонке місце — режим тунелю «Вивантажувати лише нові дані». У ньому тунель бере тільки товари зі знятим «Прапор: вивантажено».

  • Якщо товар створено через add, прапорець знятий, і товар поїде в найближчий прогін.
  • Якщо товари змінено пакетним update (з item_list), прапорець теж знімається, і зміна поїде в найближчий прогін.
  • Якщо змінено один товар без item_list, прапорець знімається, коли в запиті є хоч одне поле самого товару (ціна, залишок тощо). Запит лише з текстовими полями — назвою, описами, мета-тегами — прапорець не знімає, і в цьому режимі така зміна на сайт не поїде. Щоб поїхала, передайте в тому самому запиті flag_exported=0.

Видалення через api/scope/delete прибирає товар і з сайту, якщо в налаштуваннях тунелю дозволено видаляти товари на сайті: відразу, коли ввімкнено ще й «Видаляти дані відразу на сайті», інакше — найближчим прогоном тунелю. Без дозволу видаляти товари на сайті він лишається.

Видалене й зникле

Інкрементальна синхронізація за date_modified про видалення не дізнається. Видалений товар стирається з бази повністю — разом з описами, фото, характеристиками й привʼязками до категорій, а серед довідників get журналу видалень немає. Разом із головним товаром видаляються і його товари-опції.

Щоб зловити видалене, періодично забирайте повний перелік ідентифікаторів (limit_field=product_id, max_rows=500, обхід по page_next) і звіряйте зі своїм: чого немає в Elbuz, того вже не існує.

У зворотний бік працює те саме правило: Elbuz нічого не видаляє за вас. Товар, якого у вашій системі не стало, add і update не зачіпають — він лишається в Elbuz з останніми записаними даними. Прибрати його можна лише явним api/scope/delete з його product_id.

Відкат помилкового пакета

Якщо пакет add завів зайві чи зіпсовані товари, знайдіть їх за позначкою джерела. Товари, створені пакетом, мають у полі «Джерело: код» ваш source_code (або jumper, якщо ви його не передавали).

  • Через API: api/scope/get з фільтром filterdatafield0=source_code, filtercondition0=EQUAL, filtervalue0=my_erp дасть перелік ідентифікаторів. Для відбору саме цього прогону додайте другу умову за date_added.
  • У програмі: у таблиці товарів базового каталогу відфільтруйте колонку «Джерело: код», відмітьте знайдені й видаліть — програма перепитає «Видалити вибрані товари?» з кількістю.

api/scope/delete видаляє один товар на запит, тож відкат тисячі товарів через API — це тисяча запитів у межах частоти. Таблиця товарів видаляє всі відмічені однією дією.

Пакетне оновлення перезаписує «Джерело: код» на api. Товари, які після помилкового додавання ще й оновлювали пакетом, за вашим source_code уже не знайдуться — шукайте їх за date_added або за своїм uuid.

З помилковим update складніше: запит оновлення попередніх значень ніде не зберігає, і відкотити його засобами API не вийде. Відновлювати доведеться повторним update з правильними даними з вашої системи.

Документи: статус і залишок

Документ, створений через api/scope/add зі scope=document:

  • потребує type_id — без нього відповідь «Не вказано тип документа», з неіснуючим — «Тип документа не знайдено»;
  • лягає зі статусом «Новий», якщо status_id не передано;
  • отримує номер автоматично, за нумерацією свого типу;
  • не проводиться, якщо не передати is_sign=1. Без цього параметра залишок і резерв такий документ не змінює: резерв рахується тільки під проведені замовлення, а залишок рухає проведення. З is_sign=1 документ проводиться одразу в тому самому запиті — так само, як кнопкою проведення в програмі, з рухом залишків, а у відповіді зʼявляється поле is_sign.

Позиції документа передаються в item_list з product_id. Позиція з товаром, якого немає в базі, не записується, а у відповіді отримує статус Error і текст «Товар не знайдено»; решта документа створюється.

Від дублів при повторі захищає параметр check_unique_field_name — назва поля документа, унікального для вашої системи (наприклад, де лежить номер вашого замовлення). Якщо документ з таким значенням уже є, новий не створюється, а відповідь повертає «Документ уже існує» з document_id наявного. Для перевірки за парою полів є ще check_unique_field_name_2. Назва має бути точною назвою поля документа. Якщо поля з назвою з check_unique_field_name немає, перевірка не виконується й документ створюється; неіснуюча назва в check_unique_field_name_2 лише прибирає другу умову — перевірка йде за першим полем. Якщо значення поля в самому запиті не передали, воно вважається порожнім.

Помилки

Помилки api/scope/* приходять масивом з одного рядка:

["Увага: у вас немає дозволу на зміну даних!"]
ПовідомленняПричинаЩо робити
немає дозволу на доступ до APIтокен не переданий, сеанс закінчився або ключ вимкненоповторити api/login; якщо не допомогло — перевірити «Активність»
немає дозволу на зміну даниху ключа стоїть «Тільки для читання»зняти прапорець у картці ключа
Не задано код довідниканемає параметра scopeдодати scope або дописати його в маршрут
Невідомий код довідникадовідник не підтримується цією операцієюзвірити з таблицею довідників
Елемент не знайденовибірка порожняперевірити фільтр; це не збій
{"error":{"key":…}}вхід: не той користувач або ключзвірити пару з карткою ключа
{"error":{"ip":…}}вхід: адреса не в спискуадреса названа в тексті помилки — вписати її в ключ
{"Error":"Rate limit exceeded…"}запити частіше за дозволений проміжокпауза й повтор

Успішні запити записуються в журнал роботи як операція типу «api» разом із тілом відповіді — це найпростіший спосіб побачити, що саме робила інтеграція. Відповіді з помилкою (немає дозволу, невідомий довідник, «Елемент не знайдено» тощо) і add, який нічого не додав, у журнал не потрапляють — такі збої шукайте у відповідях, які отримала ваша інтеграція.

Сценарії

Сайт тільки забирає залишки й ціни

Задача: сайт читає з Elbuz залишки й ціни і не повинен нічого змінювати в акаунті.

Що ввімкнути: окремий ключ для сайту з прапорцем «Тільки для читання», у полі «IP-адреси для доступу» — адреса сервера сайту. Запит — api/scope/get з limit_field=sku,price,quantity, як у рецепті нижче.

Що вийде: читання працює. Будь-який add, update чи delete цим ключем відповідає «немає дозволу на зміну даних» — навіть якщо в коді сайту помилка і він спробує щось записати. Запит з іншої адреси не пройде ні на вході, ні з уже виданим токеном.

Тимчасовий доступ для стороннього розробника

Задача: підрядник налаштовує інтеграцію, і доступ має закритися разом із проєктом.

Що ввімкнути: окремий ключ, у «Дата закінчення доступу» — останній день робіт, у «Примітка» — кому й навіщо виданий.

Що вийде: ключ працює по цю дату включно. Наступного дня його не пускає ні api/login, ні токен, отриманий раніше, — чекати, поки сеанс закінчиться сам, не треба. Закінчили раніше — зніміть «Активність», діє так само. Чого не станеться: інші ключі акаунта від цього не залежать, у кожного своя дата й свій прапорець.

Облікова система шле ціни, а частину товарів менеджер веде руками

Задача: ціни й залишки приходять з обліку, але на окремих товарах менеджер ставить свою ціну, і обмін не повинен її затерти.

Що ввімкнути: на товарах менеджера — «Фіксована ціна», а там, де не можна чіпати нічого, — «Не оновлювати». Інтеграція шле api/scope/add з item_list, update_exist_product=1 і тим самим uuid, з яким товар заводила.

Що вийде: наявні товари знаходяться за uuid і оновлюються. Ціни товарів із «Фіксована ціна» лишаються менеджерськими, у товарів з «Не оновлювати» поля й характеристики не змінюються, а порожнє поле в пакеті не затирає заповнене (крім кількості, статусу й інших полів, де нуль означає дію, — перелік вище). Чого не станеться: захисту не буде, якщо ту саму зміну відправити через api/scope/update, — там «Фіксована ціна» й «Не оновлювати» поля товару не захищають, і ціна перепишеться.

Замовлення з сайту без дублів при повторі

Задача: сайт передає замовлення в Elbuz і при збої мережі повторює запит — друге таке саме замовлення зʼявитися не повинно.

Що ввімкнути: api/scope/add зі scope=document і type_id; номер замовлення сайту — в полі документа, а назва цього поля — в check_unique_field_name.

Що вийде: документ лягає зі статусом «Новий» і номером за нумерацією свого типу. Повтор з тим самим номером відповідає «Документ уже існує» з document_id першого, другий документ не створюється. Чого не станеться: залишок і резерв не зміняться, якщо не передати is_sign=1, — без нього документ лишається непроведеним, і проводять його в програмі.

Перше завантаження каталогу, яке можна прибрати

Задача: залити каталог з обліку так, щоб після обриву можна було повторити, а невдалий прогін — знайти й видалити.

Що ввімкнути: api/scope/add з item_list, source_code=my_erp, у кожного товару — власний uuid і шлях у category_name_full.

Що вийде: створені товари мають у полі «Джерело: код» значення my_erp. Товар лягає в найглибшу категорію шляху, знайдену за назвою, а якщо жодної частини шляху в базі немає — ланцюжок категорій створюється. Повтор пакета після обриву знаходить уже створене за uuid і не задвоює. Невдалий прогін знаходиться фільтром за «Джерело: код» — як це зробити. Чого не станеться: повтор не додасть фото товарам, які встигли створитися до обриву, а після api/scope/update позначка джерела зміниться на api.

Готові рецепти

Інкрементальна синхронізація каталогу

Замість того щоб щоночі викачувати весь каталог, забирайте змінене з минулого разу.

$since = file_exists('last_sync.txt') ? trim(file_get_contents('last_sync.txt')) : '2000-01-01';
$started = date('Y-m-d H:i:s');
$page = 1;

do {
    $res = elbuz_call($host, 'api/scope/get', array(
        'api_token'        => $token,
        'scope'            => 'product',
        'max_rows'         => 500,
        'limit_field'      => 'product_id,name,price,quantity,sku',
        'page'             => $page,
        'filterscount'     => 1,
        'filterdatafield0' => 'date_modified',
        'filtervalue0'     => $since,
        'filtercondition0' => 'GREATER_THAN_OR_EQUAL',
        'filteroperator0'  => 0,
    ));

    if (!isset($res['item_list'])) break;   // «Елемент не знайдено» = змін немає

    foreach ($res['item_list'] as $id => $row) {
        // ваша логіка: оновити товар у своїй системі
    }

    $page = (int)$res['meta']['page_next'];
    sleep(2);
} while ($page > 0);

file_put_contents('last_sync.txt', $started);   // час СТАРТУ, а не завершення

Зберігайте час старту прогону: те, що змінилося під час обходу, тоді потрапить у наступний прогін, а не загубиться між ними.

Видалених товарів цей цикл не побачить ніколи — як їх виявляти, розібрано в розділі про видалене й зникле.

Пакетне завантаження товарів

$products = array( /* ваш масив товарів */ );

foreach (array_chunk($products, 1000) as $chunk) {
    do {
        $res = elbuz_call($host, 'api/scope/add', array(
            'api_token'   => $token,
            'scope'       => 'product',
            'source_code' => 'my_erp',    // позначка джерела в полі «Джерело: код»
            'item_list'   => $chunk,       // у кожного товару — ваш uuid, див. нижче
        ));
        sleep(2);
    } while (isset($res['Error']));      // межа частоти — повторити ту саму пачку
}

Повтор тут іде циклом навколо тієї самої пачки. Просте continue перейшло б до наступної пачки, і відхилена тисяча товарів не потрапила б в Elbuz зовсім.

Тисяча записів у пачці — компроміс: межа 10 000, але чим більша пачка, тим довша відповідь і тим болючіший повтор, якщо запит обірвався.

Віддати сайту залишки й ціни

Найчастіша задача — і найдешевша: потрібні три поля, тож відповідь виходить маленькою, а сторінок мало.

$res = elbuz_call($host, 'api/scope/get', array(
    'api_token'   => $token,
    'scope'       => 'product',
    'max_rows'    => 500,
    'limit_field' => 'sku,price,quantity',   // product_id і name додадуться самі
));

Коли API не потрібне

Перш ніж писати інтеграцію, перевірте готові механізми — вони закривають більшість задач без розробника:

API лишається для своєї логіки: нестандартний сайт, внутрішня система обліку, обмін, якого немає в готових форматах.

Розбір частих проблем

Що ви бачитеЧому такЩо зробити
Працювало, а через годину пересталосеанс токена закінчився без зверненьперелогінитися при помилці доступу
«Rate limit exceeded» на кожному другому запитізапити йдуть частіше за проміжокпауза між запитами або пакетні операції
Приходить рівно 100 записіврозмір сторінки за замовчуваннямmax_rows до 500 і обхід по page_next
Обхід сторінок зацикливсяне перевіряється page_nextнуль означає кінець
У відповіді зайві поляобовʼязкові поля довідника дописуються завждитак і має бути
Після додавання extend зламався розбірitem_list став списком замість обʼєктарозбирати обидві форми
Порожня вибірка приходить помилкоютак відповідає get, коли нічого не знайшлосяперевіряти наявність item_list
Оновлення рядків прайсу не працюєproduct_price немає в списку оновленняоновлювати прайс завантаженням файлу
Читання працює, запис відхиляєтьсяу ключа «Тільки для читання»; прапорець діє на всі маршрути, зокрема старізняти прапорець — діє одразу, без перелогіну
Ключ перестав пускати, хоча активнийнастав день після «Дати закінчення доступу»посунути дату або очистити поле — тоді ключ безстроковий

Часті питання

Де в програмі знайти ключі API?

Налаштування системи → розділ «Інші налаштування» → плитка «API та токени доступу». Окремого пункту в головному меню немає.

Чи треба логінитися перед кожним запитом?

Ні. Сеанс живе, поки ним користуються, і видаляється після години без звернень. Новий вхід робіть у відповідь на помилку доступу.

Скільки запитів на хвилину дозволено?

За замовчуванням тридцять, і рахуються вони як мінімальний проміжок: не частіше ніж раз на дві секунди. Пачку підряд зробити не можна, навіть після довгої паузи.

Як дізнатися справжні назви полів?

Операцією api/scope/describe для потрібного довідника — вона повертає внутрішню назву, підпис і тип кожного поля.

Як синхронізувати каталог, не викачуючи його цілком?

Фільтром за date_modified із датою минулої синхронізації. Зберігайте час старту прогону, а не завершення.

Як завантажити багато товарів за раз?

Параметром item_list в операції додавання: товарів каталогу — до десяти тисяч за запит, рядків прайсу — до ста.

Чи можна орієнтуватися на код відповіді HTTP?

Ні, він завжди 200 — і на успіх, і на помилку. Розбирайте тіло: помилка приходить масивом рядків, успіх містить item_list або meta.

Суміжні теми

Чи була стаття корисною?