Вебхуки API v2 — це повідомлення, які Elbuz сам надсилає на вашу адресу, коли змінюються клієнти, документи або приймається заявка в «Пульсі». Замість того щоб щохвилини перепитувати API «що нового», ваш сервер отримує POST із переліком подій і читає змінені записи. Стаття для розробника: як підписатися, що приходить у тілі, як перевірити підпис і що Elbuz робить, коли ваш сервер не відповідає.
- Вікно
- «API та токени доступу» — підписка належить ключу API v2
- Право доступу
- ключ без «Тільки для читання»; події — лише про ресурси, які бачить власник ключа
- Підготувати
- адресу
https://на вашому сервері, доступну з інтернету
Підписка
Підписка створюється запитом API тим ключем, від якого потім читатимете записи. Ключ видається у вікні «API та токени доступу» (докладно — API v2: ключі); для підписки в ключа має бути знята позначка «Тільки для читання».
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 | принаймні одна подія з переліку нижче |
batch | 1 (за замовчуванням) — масив до 100 подій в одному запиті; 0 — одна подія на запит |
rate_per_min | скільки запитів за хвилину найбільше надішле Elbuz, від 1 до 600, за замовчуванням 60 |
status | active або 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.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,. Підписується тіло байт у байт: не розбирайте JSON до перевірки. Відкидайте запити, у яких 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.
Ваша відповідь і повтори
- Будь-який код 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.
Чого вебхуки не роблять
- Не передають дані запису. Лише id — дані читаються API з правами ключа.
- Не повідомляють про товари й прайси. Каталог синхронізується списком зі
filter[updated_since]. - Не ходять на http і внутрішні адреси. Для локальної розробки потрібен публічний https-тунель.
- Не накопичують події вимкненої підписки. Див. повтори.
Розбір частих проблем
| Що ви бачите | Чому так | Що зробити |
|---|---|---|
| 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]=… з обходом курсором.
Суміжні теми
- API v2: ключі, права, формат — як читати записи з події.
- Підключити AI-асистента через MCP.

