Inquir Compute · вебхуки на своём домене

Точка входа для вебхуков на вашем домене

Направьте Stripe, GitHub или Slack на URL с hostname, который контролируете вы, — DNS и TLS в вашем аккаунте. Проверяйте подписи по сырому телу, подтверждайте в лимите провайдера и продолжайте тяжёлую работу в serverless-функциях или пайплайнах на Inquir.

Обновлено: 2026-06-28

Суть ответа

Точка входа для вебхуков на вашем домене. Вы приносите домен и DNS; Inquir запускает изолированные serverless-функции за маршрутами шлюза. У каждого провайдера — свой обработчик с узкими правами, а не один эндпоинт, где смешаны cookie и секреты вебхуков.

Когда подходит

  • Нужен стабильный HTTPS на hostname под вашим контролем (свой домен + DNS) и serverless-обработчики на Inquir.
  • Хочется развести трафик вебхуков и пользовательские API одного продукта.

На что обратить внимание

  • Один эндпоинт на VPS смешивает ритм деплоя с приложением, прячет логи вебхуков в общем syslog и делает откат рискованным, когда меняется обработчик одного провайдера.
  • Монолитный маршрут часто смешивает cookie-сессии пользователей и машинные ключи — хуже наблюдаемость и шире зона поражения при ошибке маршрутизации.

Зачем свой URL для вебхуков

Команды держат вебхуки на своём сервере по одной главной причине: провайдеры добавляют callback URL в белый список, и стабильный hostname под вашим контролем — hooks.yourcompany.com вместо чужого SaaS-поддомена — проще объяснить службе безопасности и вписать в правила firewall.

Проверка прямо в основном API всё ещё связывает масштабирование и режимы авторизации. Отдельная точка входа на вашем домене держит машинные ключи отдельно от пользовательских сессий.

Почему VPS или монолитный эндпоинт хрупки

Один эндпоинт на VPS смешивает ритм деплоя с приложением, прячет логи вебхуков в общем syslog и делает откат рискованным, когда меняется обработчик одного провайдера.

Монолитный маршрут часто смешивает cookie-сессии пользователей и машинные ключи — хуже наблюдаемость и шире зона поражения при ошибке маршрутизации.

Свой ingress, управляемые serverless-обработчики

Вы приносите домен и DNS; Inquir запускает изолированные serverless-функции за маршрутами шлюза. У каждого провайдера — свой обработчик с узкими правами, а не один эндпоинт, где смешаны cookie и секреты вебхуков.

Привяжите свой домен к именованному API через TXT-запись для верификации и CNAME или A-запись; TLS выпускается по запросу. Сырые тела для HMAC, трассы выполнения и передача в пайплайны живут в одном воркспейсе рядом с HTTP API — без SSH на сервер ради логов вебхуков.

Возможности обработки вебхуков

Изолированные маршруты вебхуков

Разводите пути провайдеров по небольшим обработчикам с узкими правами.

Трассы выполнения

Смотрите тела (с сокрытием чувствительного) и тайминги, когда провайдер ставит доставку на паузу.

Передача в пайплайны

Быстро подтверждайте, а обработку продолжайте в пайплайне или задаче, когда работа тяжелее окна таймаута.

Как запустить вебхуки на своём домене

Привяжите свой hostname к маршрутам шлюза, проверяйте подписи в serverless-обработчиках, подтверждайте в лимите провайдера и пишите идемпотентно.

1

Направить DNS на шлюз

Привяжите свой домен и TLS — провайдеры бьют в hooks.yourcompany.com, а не в чужой поддомен.

2

Проверить и быстро подтвердить

Подпись по сырому телу, ключи идемпотентности, 200 в окне провайдера.

3

Продолжить в пайплайнах

Медленную обработку передавайте в global.durable.startNew(), чтобы ретраи не дублировали побочные эффекты.

Паттерны провайдеров на вашем домене

Шлюз на вашем hostname передаёт событие в формате AWS API Gateway: body строкой (сырые байты для подписи), заголовки как пришли. Держите по одной функции на провайдера — проверка остаётся компактной и обозримой.

webhooks/stripe.mjs
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' };
}
webhooks/github.mjs
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' };
}
webhooks/slack.mjs
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: '' };
}

Когда свой ingress на Inquir уместен

Когда это уместно

  • Нужен стабильный HTTPS на hostname под вашим контролем (свой домен + DNS) и serverless-обработчики на Inquir.
  • Хочется развести трафик вебхуков и пользовательские API одного продукта.

Когда лучше выбрать другое

  • Вебхуки уходят только в чужую iPaaS без своего рантайма.

Частые вопросы

Что делать с медленной работой на следующем шаге?

Подтвердите вебхук быстро, а работу продолжите в serverless-пайплайне или async-задаче — тогда таймауты провайдера не блокируют важные побочные эффекты.

Можно ли закрепить маршруты за тенантами?

Да. Паттерны мультитенантной маршрутизации позволяют сегментировать пути или хосты — подробности на отдельной странице фичи.

Как защититься от replay-атак?

Сочетайте проверку подписи, метки времени провайдера, где они есть, и идемпотентные записи по идентификаторам провайдера.