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
Answer first
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.
Workload and what breaks
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.
Trade-offs
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.
How Inquir helps
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.
What you get
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.
What to do next
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.
Point DNS at your gateway
Attach a custom domain and TLS so providers hit hooks.yourcompany.com—not a shared subdomain.
Verify and ack fast
Confirm signatures on the raw body, write idempotency keys, return 200 inside the provider window.
Continue in pipelines
Hand off slow fulfillment to global.durable.startNew() so retries never duplicate side effects.
Code example
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.
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: '' }; }
When it fits
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
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.