Inquir Compute · self-hosted ingress

Self-hosted webhook ingress on your domain

Point Stripe, GitHub, or Slack at URLs on a hostname you control—DNS and TLS in your account. Verify signatures on raw bodies, acknowledge within provider timeouts, and continue heavy work in serverless functions or pipelines on Inquir.

Last updated: 2026-06-28

Direct answer

Self-hosted webhook ingress on your domain. You bring the domain and DNS; Inquir runs isolated serverless functions behind your gateway routes. Each provider gets its own handler with narrow permissions—not one endpoint mixing cookies and webhook secrets.

When it fits

  • You need stable HTTPS ingress on a hostname you control (custom domain + DNS) with serverless handlers on Inquir.
  • You want isolation between webhook traffic and customer-facing APIs on the same product.

Tradeoffs

  • A single VPS endpoint mixes deploy cadence with your app, hides webhook logs in generic syslog, and makes rollback risky when one provider handler changes.
  • A monolithic app route often mixes cookie-based user auth with machine credentials, complicating observability and blast radius when something misroutes.

Why self-hosted webhook URLs are worth the setup

Teams run webhooks on their own server for one main reason: providers whitelist callback URLs, and a stable hostname you control—hooks.yourcompany.com instead of a shared SaaS subdomain—makes security reviews and firewall allowlists easier to explain.

Running verification inline with your main API still couples scaling and auth modes. A dedicated ingress path on your domain keeps machine credentials separate from user sessions.

Why a box or monolith endpoint is fragile

A single VPS endpoint mixes deploy cadence with your app, hides webhook logs in generic syslog, and makes rollback risky when one provider handler changes.

A monolithic app route often mixes cookie-based user auth with machine credentials, complicating observability and blast radius when something misroutes.

Self-hosted ingress, managed serverless handlers

You bring the domain and DNS; Inquir runs isolated serverless functions behind your gateway routes. Each provider gets its own handler with narrow permissions—not one endpoint mixing cookies and webhook secrets.

Attach a custom domain per named API with a TXT verification record plus a CNAME or A record; TLS is issued on demand. Raw bodies for HMAC, execution traces, and pipeline handoff live in one workspace next to your HTTP APIs—without SSHing into a box to tail webhook logs.

Webhook processing features

Isolated webhook routes

Map provider-specific paths to small handlers with narrow permissions.

Execution traces

Inspect bodies (redacted) and timings when a provider pauses delivery.

Async handoff to pipelines

Ack fast, continue processing in a pipeline or job when work is heavier than a timeout window.

How to run webhooks on your domain

Map a custom hostname to gateway routes, verify signatures in serverless handlers, acknowledge within provider timeouts, and apply writes idempotently.

1

Point DNS at your gateway

Attach a custom domain and TLS so providers hit hooks.yourcompany.com—not a shared subdomain.

2

Verify and ack fast

Confirm signatures on the raw body, write idempotency keys, return 200 inside the provider window.

3

Continue in pipelines

Hand off slow fulfillment to global.durable.startNew() so retries never duplicate side effects.

Provider-specific webhook patterns on your domain

Gateway passes API Gateway-style events on your hostname: body is a string (raw bytes for signing), headers as received. Keep one function per provider so verification stays small and reviewable.

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: '' };
}

Good fit

When this works

  • You need stable HTTPS ingress on a hostname you control (custom domain + DNS) with serverless handlers on Inquir.
  • You want isolation between webhook traffic and customer-facing APIs on the same product.

When to skip it

  • You only consume webhooks into a SaaS iPaaS and never touch the runtime.

FAQ

How do I handle slow downstream work?

Acknowledge the webhook quickly, then continue in a serverless async pipeline or job so provider timeouts do not stall critical side effects.

Can I pin routes per tenant?

Multi-tenant routing patterns let you segment paths or hosts; see the dedicated feature page for details.

What about replay attacks?

Combine signature verification, timestamps where supported, and idempotent writes keyed by provider identifiers.