Multi-Tenant Webhooks
Let your customers register their own webhook endpoints. Each customer gets a unique signing secret and isolated delivery.
Architecture
Instead of one endpoint for all customers, each customer gets their own HookSniff endpoint. Your app sends events to HookSniff, which delivers to each customer's URL independently.
Your App → HookSniff → Customer A's endpoint (https://a.com/webhook)
→ Customer B's endpoint (https://b.com/webhook)
→ Customer C's endpoint (https://c.com/webhook)
Each customer has:
- Unique endpoint URL
- Unique signing secret (whsec_...)
- Independent retry policy
- Independent rate limitCustomer Registration Flow
When a customer registers for webhooks, create an endpoint in HookSniff and store the details in your database:
// POST /api/webhooks/register
app.post('/api/webhooks/register', authenticate, async (req, res) => {
const { url, events } = req.body;
const customerId = req.user.id;
// 1. Validate URL (must be HTTPS)
if (!url.startsWith('https://')) {
return res.status(400).json({ error: 'URL must be HTTPS' });
}
// 2. Create endpoint in HookSniff
const endpoint = await hs.endpoint.create({
url,
description: `Customer ${customerId}`,
event_types: events, // e.g., ['order.created', 'payment.completed']
});
// 3. Save to your database
await db.webhookEndpoints.create({
customer_id: customerId,
hooksniff_endpoint_id: endpoint.id,
signing_secret: endpoint.secret, // Store encrypted!
url,
events,
created_at: new Date(),
});
// 4. Return secret to customer (only shown once)
res.json({
endpoint_id: endpoint.id,
signing_secret: endpoint.secret,
events,
url,
});
});Sending Events to All Customers
When your app generates an event, find all subscribed customers and send to each:
async function notifyCustomers(event, data) {
// Find all customers subscribed to this event
const subscribers = await db.webhookEndpoints.find({
events: { $contains: event },
active: true,
});
// Send to each subscriber's endpoint
const promises = subscribers.map(sub =>
hs.message.create({
endpoint_id: sub.hooksniff_endpoint_id,
event,
data,
})
);
await Promise.all(promises);
console.log(`Sent ${event} to ${subscribers.length} customers`);
}
// Usage
await notifyCustomers('order.created', {
order_id: 'ORD-12345',
amount: 99.99,
currency: 'USD',
});Customer Self-Service Portal
Let customers manage their own endpoints, view delivery logs, and rotate secrets — without contacting support:
// Generate a portal link for a customer
app.get('/api/webhooks/portal', authenticate, async (req, res) => {
const customerId = req.user.id;
const endpoints = await db.webhookEndpoints.find({ customer_id: customerId });
// Generate portal access for each endpoint
const links = await Promise.all(
endpoints.map(ep =>
hs.portal.generate_link({ endpoint_id: ep.hooksniff_endpoint_id })
)
);
res.json({ links });
});Handling Customer Webhooks
If customers send webhooks to you (inbound), verify each one with its unique secret:
app.post('/api/inbound-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. Identify customer from header or URL path
const customerId = req.headers['x-customer-id'];
const endpoint = await db.webhookEndpoints.findOne({ customer_id: customerId });
if (!endpoint) {
return res.status(404).json({ error: 'Unknown customer' });
}
// 2. Verify signature with customer's secret
try {
const wh = new Webhook(endpoint.signing_secret);
const payload = wh.verify(req.body, {
'webhook-id': req.headers['webhook-id'],
'webhook-timestamp': req.headers['webhook-timestamp'],
'webhook-signature': req.headers['webhook-signature'],
});
// 3. Process event
res.status(200).json({ received: true });
await processInboundEvent(customerId, payload);
} catch (err) {
res.status(401).json({ error: 'Invalid signature' });
}
});Best Practices
One endpoint per customer
Don't share endpoints between customers. Each gets their own URL and secret.
Encrypt signing secrets
Store signing secrets encrypted at rest. Decrypt only when verifying.
Use event filtering
Only send events the customer subscribed to. Use event_types on endpoint creation.
Rate limit per customer
Set per-endpoint rate limits to prevent one customer from overwhelming your system.
Offer portal access
Let customers view deliveries, rotate secrets, and manage endpoints themselves.
Don't share secrets
Never share signing secrets between customers or expose them in logs.
Full Example: SaaS Platform
A complete example for a SaaS platform where each tenant can subscribe to events:
// Database schema
CREATE TABLE webhook_endpoints (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id),
hooksniff_endpoint_id TEXT NOT NULL,
signing_secret_encrypted BYTEA NOT NULL,
url TEXT NOT NULL,
events TEXT[] NOT NULL DEFAULT '{}',
active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_webhook_endpoints_customer ON webhook_endpoints(customer_id);
CREATE INDEX idx_webhook_endpoints_events ON webhook_endpoints USING GIN(events);
// Tenant isolation middleware
function tenantIsolation(req, res, next) {
req.tenantId = req.user.tenant_id;
next();
}
// All webhook queries are scoped to tenant
app.get('/api/webhooks', tenantIsolation, async (req, res) => {
const endpoints = await db.webhookEndpoints.find({
customer_id: req.tenantId,
});
res.json({ endpoints });
});