Self-hosted ingress для вебхуков на вашем домене
Направьте Stripe, GitHub или Slack на URL вашего hostname — DNS и TLS в вашем аккаунте. Проверяйте подписи по сырому телу, подтверждайте в лимите провайдера и продолжайте тяжёлую работу в serverless-функциях или пайплайнах на Inquir.
Обновлено: 2026-06-28
Кратко
Суть ответа
Self-hosted ingress для вебхуков на вашем домене. Вы приносите домен и DNS; Inquir запускает изолированные serverless-функции за маршрутами шлюза. У каждого провайдера — свой хендлер с узкими правами, а не один эндпоинт, где смешаны cookie и секреты вебхуков.
Когда подходит и когда нет
- Нужен стабильный HTTPS на hostname под вашим контролем (custom domain + DNS) и serverless-хендлеры на Inquir.
- Хочется развести трафик вебхуков и пользовательские API одного продукта.
На что обратить внимание
- Один эндпоинт на VPS смешивает деплой с приложением, прячет логи вебхуков в общем syslog и делает откат рискованным, когда меняется обработчик одного провайдера.
- Монолитный маршрут часто смешивает cookie-пользователей и машинные ключи — хуже наблюдаемость и шире зона поражения при ошибке маршрутизации.
Ситуация: нагрузка и где обычно ломается
Зачем self-hosted URL для вебхуков
Команды держат вебхуки на своём сервере в первую очередь потому, что провайдеры добавляют callback URL в белый список: стабильный hostname под вашим контролем — hooks.yourcompany.com вместо чужого поддомена — проще объяснить службе безопасности и вписать в правила firewall.
Встроенная проверка прямо в основном API всё ещё смешивает масштабирование и режимы auth. Отдельный ingress на вашем домене держит машинные ключи отдельно от пользовательских сессий.
Компромиссы
Почему VPS или монолитный эндпоинт хрупки
Один эндпоинт на VPS смешивает деплой с приложением, прячет логи вебхуков в общем syslog и делает откат рискованным, когда меняется обработчик одного провайдера.
Монолитный маршрут часто смешивает cookie-пользователей и машинные ключи — хуже наблюдаемость и шире зона поражения при ошибке маршрутизации.
Как Inquir помогает в этом сценарии
Self-hosted ingress, управляемые serverless-хендлеры
Вы приносите домен и DNS; Inquir запускает изолированные serverless-функции за маршрутами шлюза. У каждого провайдера — свой хендлер с узкими правами, а не один эндпоинт, где смешаны cookie и секреты вебхуков.
Привяжите свой домен к именованному API через TXT-запись для верификации и CNAME или A; TLS выпускается по запросу. Сырые тела для HMAC, трассировки и передача в пайплайны — в одном рабочем пространстве рядом с HTTP API, без SSH на сервер ради просмотра логов вебхуков.
Что вы получаете на платформе
Возможности обработки вебхуков
Изолированные маршруты вебхуков
Разводите пути провайдеров по небольшим хендлерам с узкими правами.
Трассировка выполнения
Смотрите тела (при необходимости сокращённые) и тайминги, когда провайдер ставит доставку на паузу.
Передача в пайплайны
Отвечайте 200 быстро, а тяжёлую обработку продолжайте в пайплайне или задаче, когда окна HTTP не хватает.
Что сделать дальше, по шагам
Как запустить вебхуки на своём домене
Привяжите custom domain к маршрутам шлюза, проверяйте подписи в serverless-хендлерах, подтверждайте в лимите провайдера, пишите идемпотентно.
Настроить DNS на шлюз
Custom domain и TLS — провайдеры бьют в hooks.yourcompany.com, а не в чужой поддомен.
Проверить и быстро ACK
Подпись по сырому телу, ключ идемпотентности, 200 в окне провайдера.
Продолжить в пайплайнах
Тяжёлую обработку — через global.durable.startNew(), чтобы повторы не дублировали эффекты.
Пример кода
Паттерны провайдеров на вашем домене
Шлюз на вашем hostname отдаёт событие в духе AWS API Gateway: body строкой, заголовки как пришли. По одной функции на провайдера — верификация остаётся компактной.
import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export async function handler(event) { const rawBody = event.body ?? ''; const sig = event.headers['stripe-signature'] ?? ''; let evt; try { evt = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { return { statusCode: 400, body: `Webhook Error: ${err.message}` }; } const isNew = await db.upsertWebhookEvent(evt.id, evt.type); if (!isNew) return { statusCode: 200, body: 'duplicate' }; await global.durable.startNew('stripe-fulfillment', undefined, { eventId: evt.id, type: evt.type, data: evt.data.object }); return { statusCode: 200, body: 'accepted' }; }
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const sigHeader = (event.headers['x-hub-signature-256'] ?? '').replace('sha256=', ''); const expected = createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET).update(body).digest('hex'); if (sigHeader.length !== expected.length || !timingSafeEqual(Buffer.from(sigHeader, 'hex'), Buffer.from(expected, 'hex'))) { return { statusCode: 401, body: 'invalid signature' }; } const eventType = event.headers['x-github-event']; const payload = JSON.parse(body); if (eventType === 'push') { await global.durable.startNew('index-repo', undefined, { repo: payload.repository.full_name, sha: payload.after }); } return { statusCode: 200, body: 'accepted' }; }
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const timestamp = event.headers['x-slack-request-timestamp'] ?? ''; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return { statusCode: 400, body: 'stale request' }; } const sigBase = `v0:${timestamp}:${body}`; const expected = 'v0=' + createHmac('sha256', process.env.SLACK_SIGNING_SECRET).update(sigBase).digest('hex'); const received = event.headers['x-slack-signature'] ?? ''; if (expected.length !== received.length || !timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { return { statusCode: 401, body: 'invalid signature' }; } const params = Object.fromEntries(new URLSearchParams(body)); await global.durable.startNew('slack-command', undefined, { command: params.command, userId: params.user_id, channelId: params.channel_id, text: params.text, responseUrl: params.response_url, }); return { statusCode: 200, body: '' }; }
Когда подходит и когда нет
Когда self-hosted ingress на Inquir уместен
Когда это уместно
- Нужен стабильный HTTPS на hostname под вашим контролем (custom domain + DNS) и serverless-хендлеры на Inquir.
- Хочется развести трафик вебхуков и пользовательские API одного продукта.
Когда лучше выбрать другое
- Вебхуки уходят только в чужую iPaaS без своей среды выполнения.
Вопросы и ответы
Вопросы и ответы
Что делать с медленным downstream?
Подтвердите вебхук быстро, а работу продолжите в serverless-пайплайне или фоновой задаче — тогда таймауты провайдера не блокируют важные побочные эффекты.
Можно ли разводить маршруты по клиентам?
Да. Паттерны мультитенантной маршрутизации позволяют сегментировать пути или хосты — подробности на отдельной странице фичи.
Как защититься от повторных доставок?
Сочетайте проверку подписи, метки времени провайдера, где они поддерживаются, и идемпотентные записи по идентификатору события провайдера.