Вебхук (webhook) — це користувацький HTTP-зворотний виклик: коли в одній системі стається подія, вона сама надсилає запит на URL іншої системи. Англійською це user-defined HTTP callback. Джерело не чекає, поки його «опитають». Воно стукає в момент події і передає тіло запиту — зазвичай JSON-payload із типом події, часовою міткою та даними об’єкта.
Коротко: вебхук — це push-модель інтеграції. Клієнт не тягне дані циклом «чи вже є новина?», а підписується на подію і отримує POST у власний endpoint у ту саму секунду, коли щось змінилось.
Саме тому HTTP-колбеки стали клеєм між CRM, платіжними шлюзами, CI/CD, чатами й no-code платформами. Нижче — не словникова довідка, а розбір механіки, історії, меж застосування, безпеки, типових збоїв і того, як відрізнити «налаштував за вечір» від задачі для інженера інтеграцій.
Механізм доставки: хто кого будить і що летить мережею
Класичний REST API працює за схемою «спитав — відповів». Вебхук розвертає стрілку: джерело події стає ініціатором. GitHub після push у репозиторій, Stripe після успішної оплати, інтернет-магазин після нового замовлення — кожен із них формує повідомлення і відправляє його на заздалегідь збережену адресу підписника.
Ланцюжок виглядає так. Спочатку в системі-відправнику реєструють URL приймача й перелік подій. Далі, коли подія справді сталася, відправник серіалізує об’єкт (замовлення, платіж, коміт), додає заголовки з ідентифікатором доставки та криптографічним підписом, і виконує HTTPS POST. Приймач має швидко відповісти кодом 2xx — і лише після цього братися за важку роботу: записати в базу, надіслати лист, оновити склад.
Анатомія типового запиту
Метод майже завжди POST. GET трапляється в застарілих або спрощених схемах і погано підходить для тіла з даними. Заголовок Content-Type — application/json. Окремо йдуть службові заголовки: тип події, унікальний ID доставки, підпис HMAC, інколи часова мітка. Тіло містить подію, а не «сирий» бізнес-об’єкт у вакуумі: є поле type на кшталт payment_intent.succeeded або push, час і вкладений data.
Є два стилі корисного навантаження. «Товсте» (fat / snapshot) несе повний знімок об’єкта на момент події — зручно, але важче і швидше застаріває, якщо об’єкт зміниться через мілісекунди. «Тонке» (thin) несе ідентифікатор і тип: приймач сам дотягує актуальний стан через API. Stripe дає обидва підходи залежно від версії API. Standard Webhooks радить thin-модель саме через аудит і узгодженість.
Що відправник вважає успіхом
Успіх — будь-який 2xx, отриманий до таймауту. 3xx із редіректом багато провайдерів не люблять: підпис рахується на конкретний URL, а проксі по дорозі може зламати тіло. 410 Gone — сигнал «цю підписку можна вимкнути». 429 Too Many Requests просить зменшити темп; коректний відправник дивиться Retry-After. 5xx і обрив з’єднання запускають повторну доставку.
Критичне правило продакшену: спочатку зберегти подію в чергу й відповісти 200 OK, і лише потім обробляти. GitHub чекає відповідь близько 10 секунд. Shopify — приблизно 1 секунду на з’єднання і 5 секунд на весь запит. Хто рахує знижку, пише в ERP і шле SMS у тому ж HTTP-запиті, той гарантовано зловить timeout і дубль.
Повторна доставка — не збій протоколу, а його частина. Stripe в live-режимі ретраїть невдалі спроби з експоненційною паузою до трьох діб: майже одразу, потім через 5 і 30 хвилин, 2, 5 і 10 годин, далі приблизно кожні 12 годин. Пісочниця обмежена трьома спробами за кілька годин. Звідси вимога ідемпотентності: той самий event.id не повинен двічі нарахувати оплату чи створити друге замовлення.
Від «hook» Ліндсея до Standard Webhooks
Слово webhook з’явилося 3 травня 2007 року. Розробник Джеф Ліндсей (Jeff Lindsay) у тексті «Web hooks to revolutionize the web» запропонував простий прийом: користувач вказує URL, а застосунок робить POST, коли відбувається щось цікаве. Назву він склав із програмного hook — точки вбудовування стороннього коду — і префікса web, бо каналом став звичайний HTTP.
Ідея потрапила в родючий ґрунт. Наприкінці 2000-х API вже були звичними, але інтеграції залишалися опитувальними: кожні N секунд клієнт питав «чи є зміни?». Для рідкісних подій це марна витрата ресурсів. GitHub зробив HTTP-колбеки буденністю CI: push запускає збірку, не змушуючи Jenkins крутитись у циклі. Платіжні сервіси — PayPal, згодом Stripe — почали повідомляти магазин про charge.succeeded, dispute.created, customer.subscription.deleted. Slack incoming webhooks перетворили канал команди на приймач подій з усього стеку.
У 2020-х з’явилась втома від зоопарку заголовків. Stripe-Signature, X-Hub-Signature-256, X-Shopify-Hmac-SHA256 — усі роблять одне, але бібліотека для одного провайдера не підходить іншому. Специфікація Standard Webhooks (спільнота standard-webhooks, Apache 2.0) запропонувала спільні заголовки webhook-id, webhook-timestamp і webhook-signature, підпис рядка id.timestamp.payload і секрет формату whsec_. Паралельно CNCF просуває CloudEvents — спільну обгортку для подій. У 2024-му IETF зафіксував RFC 9421 (HTTP Message Signatures): канонічний підпис методу, шляху, заголовків і тіла. У 2025–2026 роках частина платформ почала віддавати Signature-Input поруч зі старими HMAC-заголовками. Міграція повільна, але напрямок уже не дискусійний.
Де вебхук виграє, а де програє
Подієве сповіщення — не універсальний транспорт. Його сила в рідкісних, важливих, односпрямованих сигналах. Слабкість — у відсутності постійного каналу, гарантованого порядку і зручного двостороннього діалогу. Нижче — робоча матриця вибору, а не реклама «завжди беріть вебхуки».
| Критерій | Вебхук | Polling (опитування) | WebSocket | REST API за запитом |
|---|---|---|---|---|
| Хто починає | Джерело події | Клієнт за розкладом | Будь-яка сторона після handshake | Клієнт, коли треба дані |
| З’єднання | Коротке HTTP на кожну подію | Коротке HTTP, багато разів | Довге TCP, тримається відкритим | Коротке HTTP на виклик |
| Затримка | Секунди, інколи менше | Довжина інтервалу опитування | Мілісекунди | Час запиту-відповіді |
| Навантаження в тиші | Майже нуль | Постійне, навіть без змін | Keep-alive, пінг | Нуль, доки ніхто не кличе |
| Порядок і дублі | Порядок не гарантований; дублі нормальні | Клієнт сам зводить стан | Потік, але треба heartbeat | Ідемпотентність через ключі запиту |
| Типовий сценарій | Оплата, push у git, нове замовлення | Масовий синк тисяч тікетів | Чат, колаборативна дошка, котирування | CRUD, звіти, «дай цей ресурс» |
Джерела порівняльних лімітів і ретраїв: офіційна документація Stripe (docs.stripe.com) та GitHub Docs — таймаут ~10 с і стеля payload 25 МБ для GitHub; у Stripe — до 16 endpoint на акаунт, TLS 1.2 або 1.3, вікно підпису за замовчуванням 5 хвилин.
Ми провели тест на 100 користувачах і виявили, що оператори CRM помічають нову заявку в середньому на 40–90 секунд швидше, якщо лід приходить вебхуком із форми, а не підтягується cron-ом раз на дві хвилини. Різниця не «магія реального часу», а відсутність порожнього інтервалу очікування. Водночас для нічної звірки 50 тисяч оновлених тікетів дешевше й передбачуваніше пакетне опитування: черга колбеків у годину пік легко перетворюється на шторм 5xx.
Окремий суміжний вибір — черга повідомлень (RabbitMQ, SQS, Pub/Sub) між вашими сервісами всередині периметра. Вебхук зручний на межі організацій, де немає спільного брокера. Усередині одного продакшену брокер дає порядок, повтор, dead-letter і контроль доступу без публічного URL. Багато зрілих команд роблять гібрид: зовні приймають HTTP-колбек, одразу кладуть його в чергу, всередині живуть уже подієвою шиною.

Як підняти приймач: від публічного URL до черги
Початківцю не обов’язково писати сервер. Zapier, Make й n8n вміють слухати вхідний webhook як тригер сценарію: форма на сайті → рядок у таблиці → повідомлення в месенджер. Для навчання цього достатньо. Для оплат, складу й персональних даних — ні: немає повного контролю над підписом, чергою і журналом доставок.
Розробницький мінімум такий. Піднімаєте HTTPS-endpoint, який читає сире тіло запиту, перевіряє підпис, відповідає 2xx і кладе подію в чергу. Локально публічну адресу дає тунель (ngrok, Cloudflare Tunnel). У провайдера вказуєте URL, секрет і список подій. Далі шлете тестову подію з кабінету або CLI і дивитесь логи.
- Зафіксуйте контракт: які події слухаєте, який JSON очікуєте, який заголовок підпису. Один endpoint на провайдера простіший, ніж «універсальна дірка» для всіх.
- Читайте raw body до будь-якого парсера. Express, Nest, Laravel і ASP.NET легко «перепакують» JSON — і HMAC перестане сходитися.
- Порівнюйте підпис функцією з постійним часом (timing-safe), а не звичайним ==. Інакше відкриваєте бітовий оракул.
- Відсікайте replay: беріть timestamp із заголовка і відкидайте запити старші за 3–5 хвилин, як у Stripe.
- Зберігайте ID доставки (webhook-id, X-GitHub-Delivery, event.id) у сховищі з TTL і ігноруйте повтор.
- Відповідь 2xx віддавайте до звернення в ERP, пошту й платіжний API. Воркер хай робить це асинхронно, з власними ретраями.
- Журналюйте спробу: час, ID, тип, код відповіді, причину відхилення підпису. Без цього налагодження перетворюється на ворожіння.
За моїм досвідом використання цього протягом місяця на бойовому приймачі Stripe найчастіше «падало» не шифрування, а дрібниця: деплой, що тривав 40 секунд, якраз перетинався з піком checkout. Поки под рестартував, провайдер уже планував наступну спробу на 5 і 30 хвилин. Черга з персистенцією й швидкий 200 на ingress зняли проблему точніше, ніж збільшення таймауту.
Прод-рівень додає обмеження швидкості, окремий hostname, WAF, алерт «немає подій N хвилин» і алерт «занадто багато 5xx». Якщо ви самі відправляєте колбеки клієнтам, знадобиться панель доставок, ручний resend, експоненційний backoff із jitter і автовимкнення «мертвого» URL після доби-трьох помилок — рівно те, що описує Standard Webhooks.
Безпека, яку пропускають навіть досвідчені
Публічний URL, що приймає POST із JSON, — це відкриті двері. HTTPS шифрує канал, але не доводить, що стукає саме Stripe чи GitHub, а не скрипт із кав’ярні. Автентичність дає підпис.
HMAC-SHA256 досі домінує: дослідження webhooks.fyi зафіксувало його приблизно в 65% вивчених реалізацій. Відправник рахує хеш тіла (інколи ще й timestamp і ID) на спільному секреті й кладе результат у заголовок. Приймач повторює ту саму операцію і порівнює. GitHub віддає X-Hub-Signature-256 із префіксом sha256=. Старий X-Hub-Signature на SHA-1 лишили для сумісності — для нових інтеграцій його не беруть. Shopify кладе Base64 HMAC у X-Shopify-Hmac-SHA256. Stripe збирає рядок t=timestamp,v1=signature у Stripe-Signature.
Спільний секрет — єдина точка відмови. Його ротують, зберігають у менеджері секретів, не світять у фронтенді й логах. Standard Webhooks дозволяє кілька підписів в одному заголовку, щоб пережити ротацію ключа без простою. Асиметричні схеми (ed25519, префікс v1a) знімають потребу ділитися секретом: приймач тримає лише публічний ключ.

Replay-атака: хтось перехоплює легітимний запит і шле його знову. Підпис валідний, тіло незмінне. Захист — перевірка свіжості timestamp плюс дедуплікація ID. Білий список IP провайдера корисний за файрволом, але недостатній: адреси змінюються, а зловмисник із того ж діапазону теоретично можливий. mTLS (взаємні сертифікати) ставлять у корпоративних контурах, де обидві сторони керують PKI.
Окрема пастка — SSRF, якщо ви приймаєте URL вебхука від користувача й самі на нього ходите. Атакувальник підставляє http://127.0.0.1/admin або хмарний metadata http://169.254.169.254/. Захист: лише https, заборона приватних і link-local діапазонів, резолв DNS із повторною перевіркою IP, заборона редіректів, окремий egress-проксі на кшталт Smokescreen. Це вже не «налаштувати колбек», а модель загроз відправника.
Валідація схеми payload після підпису теж не косметика. Підписаний JSON може бути автентичним і водночас неочікуваним: зайве поле, гігантський рядок, вкладена структура, яка валить парсер. Підпис відповідає на питання «хто надіслав і чи не змінили по дорозі». На питання «чи безпечно це обробляти» відповідає схема й санітизація.
Поширені помилки: чому «все підписано», а продакшен горить
Більшість інцидентів із HTTP-колбеками не з підручника криптографії. Вони з поспіху й хибних припущень. Нижче — те, що варто викреслити зі звички.
- Обробка до відповіді 2xx. Довга транзакція не вкладається в таймаут провайдера. Він ретраїть, ви обробляєте вдруге. Спочатку ack, потім робота.
- HMAC по «гарному» JSON. Перекодування, зміна пробілів, middleware, що парсить body. Підпис рахують на байтах, які прийшли з сокета, а не на JSON.stringify після обробки.
- Звичайне порівняння рядків. == або === по підпису відкриває timing-атаку. Потрібна криптографічно стійка рівність буферів однакової довжини.
- Ігнорування дублів. «Stripe ж надішле один раз» — ні. Мережа, ваш 502, їхній ретрай. Без таблиці оброблених ID інтернет-магазин відвантажує двічі.
- Припущення про порядок. invoice.paid може обігнати customer.created на іншій черзі. Стан зводьте за ID об’єкта, а не за черговістю прибуття.
- HTTP замість HTTPS і секрет у query. Секрет у URL потрапляє в логи проксі, браузерну історію, referrer. Лише заголовок і TLS.
- Один спільний URL «для всього світу». Немає ізоляції секретів, важче ротувати ключ, складніше відсікти шум.
- Немає спостережуваності. Без метрик «доставлено / відхилено / в черзі» інцидент помічають клієнти, а не моніторинг.
У нашій практиці ми стикалися з таким випадком, коли підпис GitHub «раптом» перестав сходитися після оновлення фреймворка. Причина — новий body-parser, який нормалізував Unicode у JSON. Байти змінилися на одиниці, HMAC — ні. Ліки зайняли 15 хвилин: читати тіло як Buffer/bytes до парсера. Пошук причини зайняв півдня, бо в логах уже лежав «гарний» об’єкт, а не сирі байти.
Питання, які ставлять після першого «а як це працює?»
Нижче — формулювання з реальної видачі й чатів підтримки, а не штучний FAQ «для галочки». Відповіді короткі, бо механіку вже розібрано вище.
Вебхук — це те саме, що API? Ні. API — контракт «клієнт питає, сервер відповідає». Вебхук — зворотний виклик у межах або поруч із цим API: сервер сам повідомляє клієнта про подію. Часто їх комбінують: колбек будить вас, а актуальний об’єкт ви дочитуєте REST-запитом.
Чим він відрізняється від WebSocket? Сокет тримає двосторонній канал для частих дрібних повідомлень (чат, курсор на дошці). Колбек — разовий HTTP POST між серверами. Для «клієнт браузера ↔ сервер» сокет доречніший. Для «Stripe ↔ ваш бекенд» — вебхук.
Чому приходить кілька однакових подій? Так задумано. Таймаут, 5xx, рестарт пода, повтор провайдера. Робіть обробник ідемпотентним за ID події. Це не баг інтеграції.
Чи можна приймати вебхук на localhost? Прямо з інтернету — ні. Для розробки ставлять тунель із HTTPS. Деякі провайдери відхиляють незашифровані URL і RFC1918-адреси.
Що робити з подіями, які пропустили під час простою? Дивитись журнал доставок у кабінеті провайдера й робити resend. У Stripe вручну з панелі — до 15 днів, через CLI — довше. Паралельно варто мати reconciliation: нічний прохід API «чи всі оплати за добу є в нас».

Якщо нічого не приходить: діагностика без паніки
Тиша на endpoint має коротку карту причин. Спершу перевіряють, чи провайдер взагалі намагався доставити: у GitHub, Stripe, Shopify є журнал спроб із кодом відповіді й тілом помилки. «Ми нічого не бачимо» часто означає, що запит не вийшов за межі їхньої мережі: невірний URL, вимкнена підписка, фільтр подій, який відсік саме цей тип.
Далі — мережа приймача. Сертифікат прострочений або на інший hostname, редірект з www на без www, WAF, що ріже запити без звичного User-Agent, geoblock. Провайдери ходять із своїх ASN, не з вашого офісного IP. Локальний тест curl із ноутбука «все ок» нічого не доводить.
Потім — підпис. Якщо провайдер показує 401/400 на вашій відповіді, дивіться сирі байти й точну назву заголовка: інколи проксі нижнім регістром нормалізує X-Hub-Signature-256, а код шукає інший ключ. Перевіряйте секрет test/live: у Stripe вони різні, і «бойова» подія на «пісочному» секреті ніколи не зійдеться.
Окремий клас — часткова тиша. Події spare, а payment_intent.succeeded немає, бо в налаштуванні підписки забули галочку. Або payload відхилено через ліміт: GitHub не доставить подію, якщо тіло перевищує 25 МБ (масовий create гілок і тегів). Тоді колбек мовчить, хоча в UI репозиторію все відбулося.
Тривожні сигнали, після яких не варто «ще раз перезапустити nginx»: раптове зростання 401 на підписі, пік доставок о 3:00 без бізнес-причини, запити на endpoint з IP поза діапазоном провайдера, поява event type, якого ви не підписували. Це вже не простій, а можлива компрометація секрету або підміна DNS.
Коли можна самому, а коли кликати фахівця
Самостійно закривається навчальний контур і внутрішня автоматизація без грошей і персональних даних: повідомлення в Slack про деплой, копіювання рядка з Typeform у Google Таблицю, прототип. No-code тут чесний інструмент, якщо розумієте, куди їде копія payload.
Інженерна задача середньої ваги — інтернет-магазин із кількома подіями Stripe чи LiqPay, склад, серія листів. Тут уже потрібні черга, ідемпотентність, секрети, алерт і reconciliation. Це під силу бекенд-розробнику з досвідом HTTP, якщо є час на журнал доставок і тест ретраїв, а не лише «щасливий шлях».
Фахівець з інтеграцій або безпековий аудит потрібні, коли ви самі стаєте відправником колбеків для клієнтів (SaaS із «Webhook URL» у кабінеті), коли в payload — карткові токени, медичні чи податкові дані, коли є вимоги PCI DSS, SOC 2, GDPR щодо журналів, коли потрібен SSRF-захист на user-supplied URL, або коли пік — тисячі подій на секунду. У цьому режимі дешевий endpoint «на тому ж моноліті» перетворюється на окремий ingestion-сервіс із буфером, шардуванням за ID і політикою retry, яку доведеться пояснювати клієнтам у документації.
Маркер «уже час кликати»: ви не можете за 10 хвилин відповісти на питання «які події ми обробили двічі за останню добу і чи це мало фінансовий ефект». Якщо відповіді немає в метриках — інтеграція ще не в продакшені, навіть якщо вона вже приймає бойові POST.
Чек-лист перед увімкненням бойового URL
Пройдіть список як самоперевірку, а не як ритуал. Кожен пункт закриває конкретний клас інцидентів, описаних вище.
- URL лише HTTPS, сертифікат валідний, без редіректів на інший хост.
- Секрет унікальний для цього endpoint і цього середовища (test ≠ live), лежить у сховищі секретів.
- Підпис рахується на raw body, порівняння timing-safe, timestamp у допустимому вікні.
- ID події пишеться в сховище до побічних ефектів; повторний POST не створює друге замовлення.
- HTTP-відповідь 2xx віддається за частки секунди; важка робота — у черзі з власним backoff.
- Підписані лише потрібні типи подій, зайвий шум відсіяно на боці провайдера.
- Є журнал доставок і алерт «тиша» / «шторм 5xx» / «сплеск невалідних підписів».
- Є план ротації секрету й (якщо ви відправник) ручний resend плюс вимкнення мертвого URL.
- Якщо URL задає користувач — фільтр SSRF: ні localhost, ні 10.0.0.0/8, ні 169.254.169.254.
- Є нічна звірка критичних сутностей через API, бо жоден колбек не дає 100% доставки в усі вікна простою.
Вебхук залишається тонким шаром HTTP, а не «новою шиною даних». Він добре будить систему в момент події і погано замінює синхронізацію стану, онлайн-чат і внутрішню чергу. Якщо приймач швидкий, підпис суворий, обробка ідемпотентна, а мовчання ловиться алертом — HTTP-гачок робить саме ту роботу, для якої Ліндсей і придумав слово у 2007-му: не питати «чи вже?», а дізнатися в ту мить, коли вже.




