Довідник старого API v1: запити, операції й помилки

16 хв

Через API v1 Elbuz віддає й приймає товари, категорії, контрагентів і документи POST-запитами з токеном. Кожна операція тут — з робочим прикладом на PHP і справжньою відповіддю сервера.

Коли це знадобиться:

  • Треба підтримати інтеграцію, написану на v1 — повний перелік операцій і довідників.
  • Запит відповідає кодом 200, а даних немає — помилка розбирається за структурою відповіді.
  • Приходить рівно 100 записів — сторінки до 500 записів і обхід до кінця.
  • Треба забирати лише змінене — готовий рецепт інкрементальної синхронізації.

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

Що вміє API v1

Нову інтеграцію пишіть одразу на API v2: v1 позначений застарілим (як перейти).

Де це і хто має доступ

Вікна в API немає: запити йдуть на адресу акаунта, а ключ видається у вікні «API та токени доступу».

Адреса
адреса акаунта + /admin/index.php?route= + маршрут
Ключ
картка ключа v1: поля «Користувач» і «Ключ доступу» (поля ключа)
Підготувати
ключ, створений до 14.10.2026: новий ключ працює лише з API v2

Як зробити перший запит

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

<?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]);
}

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

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

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

Тексти приходять мовою співробітника, від імені якого працює ключ — це власник акаунта. А помилка доступу без токена приходить мовою бази за замовчуванням, бо співробітник ще не визначений. Тобто два повідомлення одного 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

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

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

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

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

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

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

$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 додадуться самі
));

Що робити, коли запит не працює

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

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

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

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

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

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

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

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

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

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

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

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

Суміжні теми

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