Custom Webhook Provider Integration
Unhook works with any service that sends HTTP webhooks. If your provider isn’t listed in our documentation, you can still use Unhook by following this guide. Our universal webhook endpoint accepts any HTTP POST request and forwards it to your local development environment.
Quick Start
- Get your Unhook URL:
https://unhook.sh/wh_YOUR_ID
- Add to your provider’s webhook settings
- Start receiving webhooks locally:
unhook listen
Universal Webhook Features
Accepts All HTTP Methods
- POST (most common)
- GET (query parameters)
- PUT
- DELETE
- PATCH
Supports All Content Types
application/json
application/x-www-form-urlencoded
text/plain
application/xml
multipart/form-data
- Custom content types
All headers from the webhook provider are forwarded to your local endpoint, including:
- Authentication headers
- Content type
- Custom headers
- Signatures
Configuration Steps
1. Find Webhook Settings
Look for these common locations in your provider:
- Settings → Webhooks
- API → Webhooks
- Integrations → Webhooks
- Notifications → Webhooks
- Developer → Webhooks
2. Add Unhook URL
Enter your Unhook webhook URL:
https://unhook.sh/wh_YOUR_ID
3. Optional Query Parameters
Add query parameters to help identify the source:
https://unhook.sh/wh_YOUR_ID?source=provider-name
https://unhook.sh/wh_YOUR_ID?type=order&action=created
https://unhook.sh/wh_YOUR_ID?env=development
JSON Webhooks
Most modern APIs send JSON:
app.post('/webhook', express.json(), (req, res) => {
console.log('Headers:', req.headers);
console.log('Body:', req.body);
// Process the webhook
const event = req.body;
res.status(200).send('OK');
});
Some providers use form encoding:
app.post('/webhook', express.urlencoded({ extended: true }), (req, res) => {
console.log('Form data:', req.body);
// Convert to consistent format
const event = {
type: req.body.event_type,
data: req.body
};
res.status(200).send('OK');
});
XML Webhooks
Legacy systems might send XML:
const xmlparser = require('express-xml-bodyparser');
app.post('/webhook', xmlparser(), (req, res) => {
console.log('XML data:', req.body);
// Process XML structure
res.status(200).send('OK');
});
Raw Body Access
For signature verification:
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const rawBody = req.body;
const textBody = rawBody.toString('utf8');
// Verify signature with raw body
const signature = req.headers['x-webhook-signature'];
const isValid = verifySignature(rawBody, signature);
if (!isValid) {
return res.status(403).send('Invalid signature');
}
// Parse body based on content type
const contentType = req.headers['content-type'];
let data;
if (contentType.includes('application/json')) {
data = JSON.parse(textBody);
} else if (contentType.includes('form-urlencoded')) {
data = new URLSearchParams(textBody);
}
res.status(200).send('OK');
});
Common Webhook Patterns
Event-Based Webhooks
// Pattern: { event: "type", data: {...} }
function handleEventWebhook(payload) {
const { event, data } = payload;
switch (event) {
case 'user.created':
handleUserCreated(data);
break;
case 'order.completed':
handleOrderCompleted(data);
break;
default:
console.log(`Unknown event: ${event}`);
}
}
Action-Based Webhooks
// Pattern: { action: "created", resource: "user", data: {...} }
function handleActionWebhook(payload) {
const { action, resource, data } = payload;
const handler = `${resource}.${action}`;
switch (handler) {
case 'user.created':
createUser(data);
break;
case 'user.updated':
updateUser(data);
break;
case 'user.deleted':
deleteUser(data);
break;
}
}
Notification Webhooks
// Pattern: Direct data without wrapper
function handleNotificationWebhook(payload) {
// Some webhooks send data directly
if (payload.id && payload.status) {
updateRecordStatus(payload.id, payload.status);
}
}
Security Implementation
Generic Signature Verification
Many providers use HMAC signatures:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret, algorithm = 'sha256') {
const computed = crypto
.createHmac(algorithm, secret)
.update(payload)
.digest('hex');
// Some providers prefix the signature
const variations = [
computed,
`sha256=${computed}`,
`sha1=${computed}`,
algorithm + '=' + computed
];
return variations.some(variant =>
crypto.timingSafeEqual(Buffer.from(variant), Buffer.from(signature))
);
}
// Usage
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const signatures = [
req.headers['x-webhook-signature'],
req.headers['x-hub-signature'],
req.headers['x-signature'],
req.headers['authorization']
].filter(Boolean);
const isValid = signatures.some(sig =>
verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)
);
if (!isValid) {
return res.status(403).send('Invalid signature');
}
// Process webhook...
});
IP Allowlisting
Some providers publish their webhook source IPs:
const allowedIPs = [
'192.168.1.0/24',
'10.0.0.0/8'
// Add your provider's IPs
];
function isIPAllowed(ip) {
// Implement IP range checking
return allowedIPs.some(range => ipRangeCheck(ip, range));
}
app.use((req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (!isIPAllowed(clientIP)) {
return res.status(403).send('Forbidden');
}
next();
});
Debugging Webhooks
Log Everything Initially
app.post('/webhook', (req, res) => {
console.log('=== Webhook Received ===');
console.log('Method:', req.method);
console.log('URL:', req.url);
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('Query:', req.query);
console.log('Body:', req.body);
console.log('====================');
res.status(200).send('OK');
});
Webhook Inspector Endpoint
const webhookHistory = [];
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const webhook = {
id: Date.now(),
timestamp: new Date().toISOString(),
method: req.method,
url: req.url,
headers: req.headers,
query: req.query,
body: req.body.toString('utf8'),
contentType: req.headers['content-type']
};
webhookHistory.push(webhook);
// Keep last 100 webhooks
if (webhookHistory.length > 100) {
webhookHistory.shift();
}
res.status(200).send('OK');
});
// View webhook history
app.get('/webhook-history', (req, res) => {
res.json(webhookHistory);
});
Best Practices
1. Identify Webhook Source
Use query parameters or headers:
app.post('/webhook', (req, res) => {
const source = req.query.source ||
req.headers['x-webhook-source'] ||
'unknown';
console.log(`Webhook from: ${source}`);
// Route to appropriate handler
switch (source) {
case 'provider-a':
handleProviderA(req.body);
break;
case 'provider-b':
handleProviderB(req.body);
break;
default:
handleGenericWebhook(req.body);
}
res.status(200).send('OK');
});
2. Implement Retry Logic
Handle provider retries:
const processedWebhooks = new Map();
function getWebhookId(req) {
// Try common webhook ID headers
return req.headers['x-webhook-id'] ||
req.headers['x-request-id'] ||
req.headers['x-event-id'] ||
req.body?.id ||
req.body?.event_id;
}
app.post('/webhook', (req, res) => {
const webhookId = getWebhookId(req);
if (webhookId && processedWebhooks.has(webhookId)) {
console.log(`Duplicate webhook ${webhookId} skipped`);
return res.status(200).send('Already processed');
}
if (webhookId) {
processedWebhooks.set(webhookId, Date.now());
}
// Process webhook...
res.status(200).send('OK');
});
3. Handle Various Response Requirements
Different providers expect different responses:
app.post('/webhook', (req, res) => {
// Process webhook...
// Some providers expect JSON response
if (req.headers['accept']?.includes('application/json')) {
return res.json({ status: 'success' });
}
// Some expect specific text
if (req.headers['x-webhook-version'] === 'v2') {
return res.send('ACKNOWLEDGED');
}
// Default response
res.status(200).send('OK');
});
4. Graceful Error Handling
app.post('/webhook', async (req, res) => {
try {
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process asynchronously
await processWebhookAsync(req);
} catch (error) {
console.error('Webhook processing error:', error);
// Log for debugging but don't fail the webhook
await logError({
error: error.message,
webhook: {
headers: req.headers,
body: req.body
}
});
}
});
Common Integration Patterns
Queue for Processing
const Queue = require('bull');
const webhookQueue = new Queue('webhooks');
app.post('/webhook', async (req, res) => {
// Quick acknowledgment
res.status(200).send('OK');
// Queue for processing
await webhookQueue.add('process', {
headers: req.headers,
body: req.body,
timestamp: Date.now()
});
});
// Process webhooks from queue
webhookQueue.process('process', async (job) => {
const { headers, body } = job.data;
await handleWebhook(headers, body);
});
Multi-Provider Router
class WebhookRouter {
constructor() {
this.providers = new Map();
}
register(name, config) {
this.providers.set(name, config);
}
async handle(req, res) {
const provider = this.identifyProvider(req);
if (!provider) {
console.warn('Unknown webhook provider');
return res.status(200).send('OK');
}
const config = this.providers.get(provider);
// Verify if needed
if (config.verify) {
const isValid = await config.verify(req);
if (!isValid) {
return res.status(403).send('Forbidden');
}
}
// Process
await config.handler(req.body, req.headers);
// Respond
const response = config.response || 'OK';
res.status(200).send(response);
}
identifyProvider(req) {
// Check various indicators
if (req.headers['x-provider-name']) {
return req.headers['x-provider-name'];
}
if (req.query.provider) {
return req.query.provider;
}
// Check known patterns
for (const [name, config] of this.providers) {
if (config.identify && config.identify(req)) {
return name;
}
}
return null;
}
}
Testing Your Integration
Manual Testing with cURL
# Basic POST
curl -X POST https://unhook.sh/wh_YOUR_ID \
-H "Content-Type: application/json" \
-d '{"event":"test","data":{"message":"Hello Unhook!"}}'
# With custom headers
curl -X POST https://unhook.sh/wh_YOUR_ID \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: test-signature" \
-H "X-Custom-Header: custom-value" \
-d '{"test":true}'
# Form data
curl -X POST https://unhook.sh/wh_YOUR_ID \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "event=test&status=success"
- Webhook.site - Generate test webhooks
- RequestBin - Inspect HTTP requests
- Postman - API testing with webhook simulation
- Insomnia - REST client with webhook testing
Support
Need help with custom webhook integration?
If you’d like to see your provider added to our official documentation, please submit a request with details about the webhook format and authentication method.
Responses are generated using AI and may contain mistakes.