Transformation Examples
This collection of real-world transformation examples demonstrates the power and flexibility of Hooklistener's transformation engine. Each example includes complete code, use cases, and explanations.
Basic Examples
1. Add Metadata and Timestamps
Add processing information to every webhook:
addHandler('transform', async (request, context) => {
return {
...request,
body: {
...request.body,
_metadata: {
processed_at: new Date().toISOString(),
processor: 'Hooklistener',
organization_id: context.organizationId,
version: '1.0'
}
}
};
});
Use Case: Audit trail, debugging, or compliance requirements.
2. Filter Webhooks by Event Type
Only forward specific events to reduce noise:
addHandler('transform', async (request, context) => {
const allowedEvents = ['user.created', 'user.updated', 'payment.succeeded'];
if (!request.body.event_type || !allowedEvents.includes(request.body.event_type)) {
console.log(`Filtering out event: ${request.body.event_type}`);
// Return null to drop the webhook
return null;
}
return request;
});
Use Case: Reduce destination load by filtering irrelevant events.
3. Add Authentication Headers
Inject API keys or tokens required by destinations:
addHandler('transform', async (request, context) => {
const apiKey = process.env.DESTINATION_API_KEY;
return {
...request,
headers: {
...request.headers,
'Authorization': `Bearer ${apiKey}`,
'X-API-Key': apiKey,
'Content-Type': 'application/json'
}
};
});
Use Case: Connect to APIs that require authentication headers.
Data Format Conversions
4. Stripe to Slack Notifications
Convert Stripe payment webhooks to Slack messages:
addHandler('transform', async (request, context) => {
const stripeEvent = request.body;
if (stripeEvent.type === 'payment_intent.succeeded') {
const amount = stripeEvent.data.object.amount / 100; // Convert cents
const currency = stripeEvent.data.object.currency.toUpperCase();
return {
method: 'POST',
url: request.url,
headers: {
'Content-Type': 'application/json'
},
body: {
text: `💰 Payment received: ${currency} ${amount}`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Payment Successful*\nAmount: ${currency} ${amount}\nPayment ID: ${stripeEvent.data.object.id}`
}
}
]
}
};
}
// Skip other event types
return null;
});
Use Case: Convert payment notifications into user-friendly Slack alerts.
5. GitHub to Discord Webhook
Transform GitHub webhook format to Discord's expected format:
addHandler('transform', async (request, context) => {
const github = request.body;
if (github.action === 'opened' && github.pull_request) {
const pr = github.pull_request;
return {
method: 'POST',
url: request.url,
headers: {
'Content-Type': 'application/json'
},
body: {
embeds: [{
title: `New Pull Request: ${pr.title}`,
description: pr.body || 'No description provided',
color: 3447003, // Blue
author: {
name: pr.user.login,
icon_url: pr.user.avatar_url
},
fields: [
{
name: 'Repository',
value: github.repository.full_name,
inline: true
},
{
name: 'Branch',
value: `${pr.head.ref} → ${pr.base.ref}`,
inline: true
}
],
url: pr.html_url,
timestamp: pr.created_at
}]
}
};
}
return null; // Skip other events
});
Use Case: Display GitHub activity in Discord channels with rich formatting.
Advanced Data Processing
6. Data Enrichment with Calculated Fields
Add computed fields based on existing data:
addHandler('transform', async (request, context) => {
const order = request.body;
if (order.items && Array.isArray(order.items)) {
// Calculate totals
const subtotal = order.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0);
const tax = subtotal * 0.08; // 8% tax
const total = subtotal + tax;
// Add shipping logic
const shipping = subtotal > 100 ? 0 : 9.99;
const grandTotal = total + shipping;
return {
...request,
body: {
...order,
calculated: {
subtotal: Math.round(subtotal * 100) / 100,
tax: Math.round(tax * 100) / 100,
shipping: shipping,
total: Math.round(grandTotal * 100) / 100,
free_shipping: shipping === 0
}
}
};
}
return request;
});
Use Case: Enhance order data with calculated fields for downstream systems.
7. Dynamic Routing Based on Content
Route webhooks to different URLs based on content:
addHandler('transform', async (request, context) => {
const event = request.body;
const baseUrl = 'https://api.example.com';
// Determine route based on event type
let endpoint = '/default';
if (event.type === 'user.created') {
endpoint = '/users/new';
} else if (event.type === 'payment.succeeded') {
endpoint = '/payments/success';
} else if (event.severity === 'critical') {
endpoint = '/alerts/critical';
}
return {
...request,
url: `${baseUrl}${endpoint}`,
body: {
...event,
routing: {
original_url: request.url,
routed_to: endpoint,
reason: 'content-based routing'
}
}
};
});
Use Case: Single webhook source feeding multiple specialized endpoints.
8. Payload Validation and Sanitization
Validate and clean incoming data:
addHandler('transform', async (request, context) => {
const data = request.body;
// Validation rules
const errors = [];
if (!data.email || !data.email.includes('@')) {
errors.push('Invalid email address');
}
if (!data.phone || !/^\+?[\d\s-()]+$/.test(data.phone)) {
errors.push('Invalid phone number');
}
if (errors.length > 0) {
console.log('Validation errors:', errors);
// Could return null to drop invalid webhooks
// or transform them into error notifications
return {
...request,
body: {
error: 'Validation failed',
details: errors,
original_data: data
}
};
}
// Sanitize data
const sanitized = {
...data,
email: data.email.toLowerCase().trim(),
phone: data.phone.replace(/[^\d+]/g, ''), // Keep only digits and +
name: data.name?.trim(),
created_at: new Date().toISOString()
};
return {
...request,
body: sanitized
};
});
Use Case: Ensure data quality before processing by downstream systems.
Integration Patterns
9. Multi-Destination Conditional Forwarding
Send different data to different systems based on conditions:
addHandler('transform', async (request, context) => {
const event = request.body;
// This example assumes you'll handle routing outside the transformation
// But demonstrates how to prepare different payloads
if (event.user_tier === 'premium') {
// Enhanced data for premium user events
return {
...request,
body: {
...event,
priority: 'high',
enhanced_data: {
tier: 'premium',
special_handling: true,
notify_account_manager: true
}
}
};
} else if (event.user_tier === 'free') {
// Simplified data for free tier
return {
...request,
body: {
event_type: event.event_type,
user_id: event.user_id,
tier: 'free',
timestamp: event.timestamp
}
};
}
return request; // Standard processing
});
Use Case: Different service tiers receive different levels of webhook data.
10. Rate Limiting and Throttling Logic
Add rate limiting information to webhooks:
addHandler('transform', async (request, context) => {
const userId = request.body.user_id;
const eventType = request.body.event_type;
// Simple rate limiting logic (in production, you'd use external storage)
const now = Date.now();
const rateKey = `${userId}:${eventType}`;
// Add rate limiting metadata
return {
...request,
body: {
...request.body,
rate_limit: {
user_id: userId,
event_type: eventType,
timestamp: now,
rate_key: rateKey,
// This could trigger different handling downstream
needs_throttling: false // This would be calculated based on actual rates
}
}
};
});
Use Case: Add rate limiting context for downstream throttling systems.
Error Handling Patterns
11. Graceful Error Recovery
Handle transformation errors without breaking the webhook flow:
addHandler('transform', async (request, context) => {
try {
// Potentially risky transformation
const complexData = JSON.parse(request.body.nested_json);
const processed = complexData.items.map(item => ({
...item,
processed: true,
timestamp: new Date().toISOString()
}));
return {
...request,
body: {
...request.body,
processed_items: processed,
status: 'success'
}
};
} catch (error) {
console.log('Transformation error:', error.message);
// Return original request with error context
return {
...request,
body: {
...request.body,
transformation_error: {
message: error.message,
timestamp: new Date().toISOString(),
fallback_used: true
},
status: 'partial_failure'
}
};
}
});
Use Case: Ensure webhook delivery continues even when transformations encounter errors.
12. Retry Logic Enhancement
Add retry context to webhooks:
addHandler('transform', async (request, context) => {
const event = request.body;
// Determine retry behavior based on event criticality
let retryConfig;
if (event.type === 'payment.failed') {
retryConfig = {
max_retries: 5,
backoff: 'exponential',
critical: true
};
} else if (event.type === 'user.login') {
retryConfig = {
max_retries: 1,
backoff: 'linear',
critical: false
};
} else {
retryConfig = {
max_retries: 3,
backoff: 'exponential',
critical: false
};
}
return {
...request,
body: {
...event,
retry_config: retryConfig
}
};
});
Use Case: Different webhook types need different retry strategies.
Real-World Scenarios
13. E-commerce Order Processing
Transform order webhooks for multiple fulfillment systems:
addHandler('transform', async (request, context) => {
const order = request.body;
// Transform for fulfillment system format
const fulfillmentData = {
order_id: order.id,
customer: {
name: `${order.customer.first_name} ${order.customer.last_name}`,
email: order.customer.email,
phone: order.customer.phone
},
shipping_address: {
line1: order.shipping.address1,
line2: order.shipping.address2,
city: order.shipping.city,
state: order.shipping.province,
postal_code: order.shipping.zip,
country: order.shipping.country
},
items: order.line_items.map(item => ({
sku: item.sku,
name: item.title,
quantity: item.quantity,
price: parseFloat(item.price)
})),
totals: {
subtotal: parseFloat(order.subtotal_price),
tax: parseFloat(order.total_tax),
shipping: parseFloat(order.total_shipping),
total: parseFloat(order.total_price)
},
metadata: {
source_order_id: order.id,
source: 'shopify',
processed_at: new Date().toISOString()
}
};
return {
...request,
body: fulfillmentData
};
});
Use Case: Standardize order format across different fulfillment providers.
14. Customer Support Ticket Routing
Route support tickets based on urgency and category:
addHandler('transform', async (request, context) => {
const ticket = request.body;
// Determine urgency based on keywords and customer tier
const urgentKeywords = ['urgent', 'critical', 'down', 'not working', 'emergency'];
const isUrgent = urgentKeywords.some(keyword =>
ticket.subject?.toLowerCase().includes(keyword) ||
ticket.description?.toLowerCase().includes(keyword)
);
const isPremium = ticket.customer?.plan === 'premium';
let priority = 'normal';
let assignee = null;
if (isUrgent && isPremium) {
priority = 'critical';
assignee = process.env.PREMIUM_SUPPORT_AGENT;
} else if (isUrgent) {
priority = 'high';
} else if (isPremium) {
priority = 'medium';
}
return {
...request,
body: {
...ticket,
routing: {
priority: priority,
assignee: assignee,
auto_escalate: isUrgent && isPremium,
sla_hours: isPremium ? 4 : 24
},
analysis: {
urgent_keywords_found: isUrgent,
customer_tier: ticket.customer?.plan || 'free'
}
}
};
});
Use Case: Automatically prioritize and route support tickets.
Testing Your Transformations
When testing these examples, use the test endpoint with realistic sample data:
curl -X POST https://api.hooklistener.com/api/v1/transformations/test \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"transformation": {
"code": "YOUR_TRANSFORMATION_CODE_HERE"
},
"request": {
"method": "POST",
"url": "https://example.com/webhook",
"headers": {
"content-type": "application/json"
},
"body": {
"// YOUR_TEST_DATA_HERE"
}
}
}'
Best Practices for Production
- Always handle errors gracefully - Never let transformations break the webhook flow
- Validate input data - Check for required fields before processing
- Use meaningful console logs - Helps with debugging in production
- Keep transformations focused - One transformation per logical operation
- Test with realistic data - Use actual webhook payloads for testing
- Monitor performance - Watch execution times and memory usage
- Document your logic - Use comments to explain complex transformations
Need More Examples?
These examples cover common patterns, but webhook transformation needs are diverse. If you have a specific use case not covered here:
- Check the community examples repository
- Join our Slack community for help and inspiration
- Contact support for custom transformation assistance (Enterprise plans)
Transformations are limited only by your imagination and JavaScript skills. Start with simple examples and build complexity as you become more comfortable with the platform.