↑↓ вибірEnter відкритиEsc закрити

    1. Elbuz
    2. Керівництво користувача Elbuz
    3. AI-агенти, MCP і API

    Вебхуки API v2: як отримувати повідомлення про зміни

    13 хв читанняОновлено

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

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

      • Нові клієнти мають потрапляти у вашу CRM — подія приходить за хвилину після створення.
      • Проведене замовлення треба передати на склад — подія documents.signed на кожен документ.
      • Ваш сервер не витримує потоку — події приходять пачками до 100, з межею запитів за хвилину.
      • Сервер був недоступний — пачку надішлють знову, усього до шести спроб.

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

      Що вміють вебхуки

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

      У вікнах програми підписок немає: їх створюють і змінюють запитами .

      Ключ
      ключ API v2 без «Тільки для читання» (як видати)
      Право доступу
      події — лише про ресурси, які бачить власник ключа
      Підготувати
      адресу https:// на вашому сервері, доступну з інтернету

      Як підписатися

      Підписка створюється запитом API тим ключем, від якого потім читатимете записи. Ключ видається у вікні «API та токени доступу» (докладно — API v2: ключі); для підписки в ключа має бути знята позначка «Тільки для читання».

      Картка ключа API v2: від імені співробітника, режим змін, запобіжник і новий ключ
      1Від імені співробітника
      2Новий ключ
      3Тільки для читання
      1. Від імені співробітника — чиї права й «Чиї дані» отримає ключ. Інших співробітників тут бачить лише той, у кого є доступ до вікна користувачів.
      2. Новий ключ показується один раз: скопіюйте його кнопкою «Копіювати» до закриття вікна.
      3. Тільки для читання — у нового ключа ввімкнено; зніміть, якщо ключу треба щось змінювати.
      curl -s -X POST "https://<ваш-домен>/api/v2/webhooks" \
        -H "Authorization: Bearer elbz_…" -H "Content-Type: application/json" \
        -H "Idempotency-Key: wh-crm-sync-1" \
        -d '{"url":"https://example.com/elbuz-hook",
             "events":["contractors.created","contractors.deleted","documents.updated"],
             "description":"CRM sync","batch":1,"rate_per_min":60}'

      Відповідь 201 (демо-акаунт, 14.10.2026):

      {"data":{"secret":"whsec_biVB0fz/Dm5U0yfY59idgl5lpaXngK7Vey3ngKCZzpY=",
       "webhook_id":9,"url":"https://example.com/elbuz-hook",
       "events":["contractors.created","contractors.deleted","documents.updated"],
       "status":"active","description":"CRM sync","batch":1,"rate_per_min":60, …}}

      Секрет whsec_… показується лише в цій відповіді. У базі він зберігається зашифрованим, GET /api/v2/webhooks/9 його не повертає, а повтор запиту з тим самим Idempotency-Key віддасть «*** shown once ***». Загубили секрет — видаліть підписку й створіть нову.

      ПолеЩо задає
      urlлише https на стандартному порту, без логіна й пароля в адресі; усі IP-адреси хоста мають бути публічними
      eventsпринаймні одна подія з переліку нижче
      batch1 (за замовчуванням) — масив до 100 подій в одному запиті; 0 — одна подія на запит
      rate_per_minскільки запитів за хвилину найбільше надішле Elbuz, від 1 до 600, за замовчуванням 60
      statusactive або off; active також знімає стан error
      descriptionпримітка, до 255 символів

      Змінити — PATCH /api/v2/webhooks/{id}, видалити — DELETE (відповідь 204, журнал доставок лишається). Підписки бачить і змінює лише ключ, який їх створив: інший ключ того самого акаунта отримає 404. Заявок у «Пульсі» підписки не створюють — у будь-якому режимі ключа.

      Адреса перевіряється при створенні, при зміні й перед кожною відправкою. Відмови приходять із поясненням: url: https URL required, url: private or reserved ip, url: port not allowed. Переадресацію (3xx) Elbuz не виконує — така відповідь вважається помилкою доставки.

      На які події можна підписатися

      ПодіяКоли
      contractors.created, contractors.updated, contractors.deletedклієнта чи постачальника створено, змінено, переміщено в кошик — у вікні, імпортом, старим API чи API v2
      documents.created, documents.updated, documents.deleted, documents.signedдокумент (замовлення, рахунок, угоду) створено, змінено, видалено в кошик, проведено. documents.updated приходить і тоді, коли замовлення змінило завантаження: тунель, маркетплейси, що оновлюють замовлення (перелік), POST /orders/sync. Замовлення прийшло без змін — події немає
      documents.payment_overdueминув термін оплати документа
      pulse.approvedу «Пульсі» прийнято заявку

      Підписатися можна лише на події ресурсів, які бачить власник ключа: немає доступу до вікна клієнтів — contractors.* поверне 422 «events: no access to contractors». Подій про товари поки немає: зміни каталогу забирайте списком GET /api/v2/products?filter[updated_since]=….

      Що приходить у запиті

      При batch=1 тіло — масив до 100 подій, при batch=0 — один такий об'єкт. Приклад запиту (значення ідентифікаторів і підпису умовні):

      POST /elbuz-hook
      Content-Type: application/json
      User-Agent: Elbuz-Webhooks/1
      webhook-id: msg_3db9ec1f3f81b1fa1bf46510
      webhook-timestamp: 1791974700
      webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
      
      [{"id":"evt_96788537a68c0722d7b6fa82","type":"contractors.updated","timestamp":"2026-10-14T10:45:00Z",
        "data":{"id":19277,"resource":"contractors"}},
       {"id":"evt_1f0c2a9e44d7b8a5c3e6f701","type":"documents.signed","timestamp":"2026-10-14T10:45:02Z",
        "data":{"id":5120,"resource":"documents"}}]

      У події лише id запису. Самих даних у тілі немає: прочитайте запис GET /api/v2/{resource}/{id} своїм ключем. Так ви завжди отримуєте свіжий стан і лише ті поля, які дозволені ключу. timestamp — час події в UTC.

      Масова дія — одна подія

      Якщо в одній дії змінилося більше одного запису того самого виду — видалили виділене в сітці, змінили поле в сотні документів, пройшов імпорт, — приходить не сотня подій, а одна {resource}.bulk_{action}:

      {"id":"evt_…","type":"contractors.bulk_deleted","timestamp":"2026-10-14T10:44:58Z",
       "data":{"resource":"contractors","action":"deleted","count":50,
               "ids":[19227,19228,19229, …],"truncated":false,"operation_id":"op_3514f5e4c0df634e"}}

      В ids — до 1000 id; якщо записів більше, truncated: true, а для створених і змінених є ще list_url — список зі змінами за останню годину, який дочитується курсором.

      Окремо на bulk_* не підписуються: підписка на contractors.deleted отримує й contractors.bulk_deleted. Виняток — documents.payment_overdue: завжди окремою подією на документ і не частіше разу на 30 днів про той самий документ.

      Кілька змін одного запису

      Подія updated чекає 60 секунд перед відправкою. Якщо за цей час той самий запис змінили ще раз, нова подія не додається — ви отримаєте одну і прочитаєте вже остаточний стан. Події created, deleted, signed не чекають.

      Коли приходить

      У мить дії нічого не надсилається: події збираються до кінця дії (запиту, імпорту), потім стають у чергу підписки, а відправляє їх фонова задача. Розраховуйте на затримку до хвилини, для updated — ще 60 секунд склейки.

      Пачки йдуть по черзі — по одній підписці в польоті не буває двох запитів одночасно. Але updated через 60 секунд склейки може прийти пізніше за подію, що сталася після нього, — наприклад, після deleted того самого запису. Покладайтеся на стан із GET, а не на порядок подій.

      Як перевірити підпис

      Формат — Standard Webhooks. Підпис — HMAC-SHA256 від рядка webhook-id + "." + webhook-timestamp + "." + сире тіло; ключ — base64-частина секрету після whsec_, розкодована; результат у base64 з префіксом v1,. Підписується тіло байт у байт: не розбирайте до перевірки. Відкидайте запити, у яких webhook-timestamp відрізняється від поточного часу більше ніж на 5 хвилин.

      PHP

      $secret = 'whsec_…';
      $id   = $_SERVER['HTTP_WEBHOOK_ID'];
      $ts   = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'];
      $body = file_get_contents('php://input');
      
      $key = base64_decode(substr($secret, 6));
      $expected = 'v1,' . base64_encode(hash_hmac('sha256', $id . '.' . $ts . '.' . $body, $key, true));
      $valid = abs(time() - (int)$ts) <= 300
            && in_array(true, array_map(fn($s) => hash_equals($expected, $s),
                                        explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE'])), true);
      if (!$valid) { http_response_code(401); exit; }

      Node.js (Express)

      const crypto = require('crypto');
      app.post('/elbuz-hook', express.raw({ type: 'application/json' }), (req, res) => {
        const key = Buffer.from(process.env.ELBUZ_WHSEC.slice(6), 'base64');
        const id = req.get('webhook-id'), ts = req.get('webhook-timestamp');
        const expected = 'v1,' + crypto.createHmac('sha256', key)
          .update(id + '.' + ts + '.' + req.body.toString('utf8')).digest('base64');
        const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
        const ok = req.get('webhook-signature').split(' ').some(s =>
          s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
        if (!fresh || !ok) return res.sendStatus(401);
        res.sendStatus(200);
      });

      Python

      import base64, hashlib, hmac, time
      
      def valid(secret, headers, raw_body: bytes) -> bool:
          key = base64.b64decode(secret[6:])
          msg = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.".encode() + raw_body
          expected = "v1," + base64.b64encode(hmac.new(key, msg, hashlib.sha256).digest()).decode()
          fresh = abs(time.time() - int(headers['webhook-timestamp'])) <= 300
          return fresh and any(hmac.compare_digest(expected, s) for s in headers['webhook-signature'].split())

      Усі три приклади перевірено на підписі, обчисленому так само, як його обчислює Elbuz.

      Що відповідати і як Elbuz повторює

      Що Elbuz робить з пачкою залежно від відповіді вашого сервера
      2xxє Retry-Afterінший код або тайм-аутнітактакніПачка до 100 подійPOST на вашу адресуВідповідь сервераДоставлено, наступна пачкаПауза підписки на вказаний час,спроба не рахуєтьсяСпроб уже 6?Повтор тієї самої пачки через 1 хв,5 хв, 30 хв, 2 год, 6 годПачка failedТретя failed поспіль?Підписка в стані error, події ненакопичуються
      • Початок або кінець
      • Дія або результат
      • Розвилка: далі шлях залежить від відповіді
      • Будь-який код 2xx — пачку доставлено. Відповідайте швидко: Elbuz чекає 15 секунд (з'єднання — 5). Важку обробку виносьте у свою чергу.
      • Інший код, тайм-аут, обрив — пачку буде надіслано повторно цілком: той самий webhook-id, те саме тіло, новий webhook-timestamp і підпис. Усього 6 спроб: одразу, через 1 хвилину, 5 хвилин, 30 хвилин, 2 години, 6 годин.
      • Заголовок Retry-After (зазвичай з 429) — підписка стоїть на паузі вказану кількість секунд, до доби. Така відповідь не зараховується як спроба: поки ви відповідаєте з Retry-After, пачку надсилатимуть знову без обмеження кількості, а шість спроб рахуються лише для помилок без цього заголовка.
      • Поки пачка чекає повтору, нові події підписки чекають за нею — порядок не порушується.
      • Шість спроб вичерпано — пачка та її події отримують статус failed і більше не надсилаються. Після трьох таких пачок поспіль підписка переходить у стан error.

      Підписка в стані off або error події не накопичує: зміни, що сталися, поки вона не діяла, вже не прийдуть. Після відновлення (PATCH {"status":"active"}) дочитайте пропущене списком ?filter[updated_since]= з часу останньої успішної доставки.

      Обробка без дублів

      Пачка може прийти двічі: ваш сервер обробив її, але відповідь не дійшла. Запам'ятовуйте id кожної події (evt_…) і пропускайте вже оброблені. Саме id події, а не webhook-id: webhook-id належить пачці.

      Журнал доставок

      GET /api/v2/webhook_deliveries?filter[webhook_id]=9 — останні 200 пачок підписки, новіші зверху:

      {"delivery_id":51,"msg_id":"msg_3db9ec1f3f81b1fa1bf46510","types":"contractors.updated",
       "events_count":"50","status":"sent","attempts":"1","response_code":"200","error":"",
       "next_try":null,"date_try":"…","date_sent":"…"}

      status: queued — готується, retry — чекає повтору до next_try, sent — доставлено, failed — спроби вичерпано. Журнал і події зберігаються 30 днів.

      Сценарії

      Нові клієнти — у вашу CRM

      Підписка на contractors.created. На кожну подію — GET /api/v2/contractors/{id}; на contractors.bulk_created після імпорту — GET /api/v2/contractors?filter[contractor_id][gte]=… або перебір ids.

      Проведене замовлення — на склад

      Підписка на documents.signed, batch=0, якщо ваш склад приймає по одному замовленню. Рядки — GET /api/v2/document_items?filter[document_id]={id}.

      Повільний приймач

      Сервер витримує 10 запитів за хвилину: rate_per_min=10 і batch=1 — до 1000 подій за хвилину пачками по 100. У піку відповідайте 429 з Retry-After: 60.

      Що робити, коли події не приходять

      Розбір частих проблем
      Що ви бачитеЧому такЩо зробити
      422 «url: https URL required»адреса на httpлише https (підписка)
      422 «url: private or reserved ip»хост вказує на внутрішню адресупублічна адреса або тунель
      422 «events: unknown event …»події немає в перелікудив. події; на bulk_* окремо не підписуються
      Підпис не сходитьсяпідписується перетворене тіло або ключ не розкодовано з base64перевіряйте сирі байти тіла (перевірка підпису)
      Ті самі події прийшли двічіповтор пачки після помилки чи тайм-аутупропускайте оброблені evt_…
      Події перестали приходити, status: errorтри пачки поспіль не доставленоPATCH {"status":"active"} і дочитати пропущене

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

      Де взяти секрет підпису вебхука?

      Лише у відповіді на створення підписки, поле secret (whsec_…). Пізніше його не показати; загубили — створіть нову підписку.

      Чому прийшла одна подія замість сотні?

      Масова дія (видалення виділеного, масова правка, імпорт) дає одну подію {resource}.bulk_{action} з count і ids до 1000.

      Скільки разів Elbuz повторює доставку?

      Шість спроб: одразу, через 1 хв, 5 хв, 30 хв, 2 год і 6 год. Відповідь із Retry-After ставить паузу до доби й спробою не вважається.

      Чому в події немає даних клієнта?

      Подія містить лише id. Записи читаються GET /api/v2/{resource}/{id} — зі свіжим станом і правами ключа.

      Чи є вебхуки про зміну товарів?

      Ні. Для каталогу — список GET /api/v2/products?filter[updated_since]=… з обходом курсором.

      Як перевірити вебхуки на своєму комп'ютері?

      Лише через публічний https-тунель: адреси на http і внутрішні Elbuz відхиляє (вимоги до адреси).

      Суміжні теми

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

      Зміст статті

      Спробувати безкоштовно