Custom Webhook Integration
Send events to your own backend or third-party APIs with control over data transformation, batching, and delivery.
Configuration
Configure the custom webhook provider with your endpoint and optional settings:
providers: {
custom: {
enabled: true,
settings: {
// Required
endpoint: 'https://api.yourapp.com/analytics',
// Optional headers
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json',
'X-Store-ID': 'store-123'
},
// Batching settings
batchSize: 10, // Events per batch
batchIntervalMs: 5000, // Max time before sending
// Retry configuration
maxRetries: 3,
retryDelayMs: 1000, // Exponential backoff: 1s, 2s, 4s
// Transform events before sending
transformFunction: (event) => {
return {
...event,
app_version: '1.0.0',
environment: 'production',
timestamp: new Date().toISOString()
};
}
}
}
}
Configuration Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
endpoint | string | Yes | - | The URL of your backend endpoint that receives analytics events |
headers | object | No | {} | Custom HTTP headers to include in each request (e.g., auth tokens) |
batchSize | number | No | 10 | Number of events to accumulate before sending a batch |
batchIntervalMs | number | No | 5000 | Maximum milliseconds to wait before sending a batch, regardless of size |
maxRetries | number | No | 3 | Number of times to retry failed requests |
retryDelayMs | number | No | 1000 | Initial delay in milliseconds for retry attempts (exponential backoff) |
transformFunction | function | No | - | Function to transform each event before sending (receives event, returns modified event) |
Request Format
Events are sent as POST requests in batches to your endpoint:
POST https://api.yourapp.com/analytics
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"events": [
{
"event": "dl_add_to_cart",
"event_id": "sess_123_1_1234567890",
"event_time": "2025-01-12T10:30:00Z",
"ecommerce": {
"items": [
{
"item_id": "SKU123",
"item_name": "Product Name",
"price": 29.99,
"quantity": 1
}
],
"value": 29.99,
"currency": "USD"
},
"user_properties": {
"user_id": "user_123",
"session_id": "sess_123",
"country": "US"
},
"attribution": {
"source": "google",
"medium": "cpc",
"campaign": "summer_sale"
}
},
{
"event": "dl_view_item",
"event_id": "sess_123_2_1234567891",
"event_time": "2025-01-12T10:30:02Z",
...
}
],
"batch_info": {
"size": 2,
"timestamp": "2025-01-12T10:30:05Z",
"source": "next-campaign-cart"
}
}
Request Body Fields
| Field | Type | Description |
|---|---|---|
events | array | Array of event objects (max 100 per batch) |
batch_info.size | number | Number of events in this batch |
batch_info.timestamp | string | ISO 8601 timestamp when batch was sent |
batch_info.source | string | Always "next-campaign-cart" |
Response Expected
Your endpoint should return appropriate HTTP status codes:
200 OK- Events processed successfully201 Created- Events created successfully202 Accepted- Events accepted for processing (recommended)4xx- Client error (SDK will retry based on specific status)5xx- Server error (SDK will retry with exponential backoff)
Response Body (Optional)
While not required, you can return a response body:
{
"success": true,
"message": "Analytics events received",
"batch_id": "batch_abc123xyz",
"events_processed": 2
}
Batching Behavior
Events are batched to reduce network requests and server load. Batches are sent when:
- Size Threshold:
batchSizeevents have been queued - Time Threshold:
batchIntervalMsmilliseconds have elapsed since the first event in the batch - Page Unload: Immediately before navigation or window close to ensure data is not lost
- Manual Flush: When explicitly requested through the SDK API (if available)
Example Batching Timeline
With batchSize: 10 and batchIntervalMs: 5000:
Time Event Batch Status
0ms Event 1 arrives Queue: [1] → Start 5s timer
100ms Event 2 arrives Queue: [1, 2]
500ms Event 3 arrives Queue: [1, 2, 3]
1000ms Event 4 arrives Queue: [1, 2, 3, 4]
1500ms Event 5 arrives Queue: [1, 2, 3, 4, 5]
2000ms Event 6 arrives Queue: [1, 2, 3, 4, 5, 6]
2500ms Event 7 arrives Queue: [1, 2, 3, 4, 5, 6, 7]
3000ms Event 8 arrives Queue: [1, 2, 3, 4, 5, 6, 7, 8]
3500ms Event 9 arrives Queue: [1, 2, 3, 4, 5, 6, 7, 8, 9]
4000ms Event 10 arrives Queue: [1-10] → SEND BATCH (size threshold met)
4050ms Event 11 arrives Queue: [11] → Start new 5s timer
5000ms (5s elapsed) Queue: [11-15] → SEND BATCH (time threshold met)
Retry Logic
Failed requests retry with exponential backoff to prevent overwhelming your server during temporary outages.
Retry Schedule
- Attempt 1: Immediate, no delay
- Attempt 2: Wait
retryDelayMs(default: 1s) - Attempt 3: Wait
retryDelayMs * 2(default: 2s) - Attempt 4: Wait
retryDelayMs * 4(default: 4s)
After maxRetries attempts, events are dropped and logged.
Example with Default Settings
Time Event Status
0ms Send batch Fails (500 error)
0ms Schedule retry 1
1000ms Retry attempt 1 Fails (500 error)
1000ms Schedule retry 2
3000ms Retry attempt 2 Fails (503 timeout)
3000ms Schedule retry 3
7000ms Retry attempt 3 Succeeds (200 OK)
Batch sent!
Retry Configuration Examples
Aggressive Retries (for critical analytics):
maxRetries: 5,
retryDelayMs: 500, // 0.5s, 1s, 2s, 4s, 8s = 15.5s total
Conservative Retries (for high-volume events):
maxRetries: 2,
retryDelayMs: 2000, // 2s, 4s = 6s total
No Retries (if endpoint is highly available):
maxRetries: 0, // Send once, drop on failure
Transform Function
The transformFunction option allows you to modify, filter, or enrich events before they're sent to your backend. This function is called once for each event.
Basic Transform Function
transformFunction: (event) => {
// Add custom fields
event.app_version = '1.0.0';
event.environment = 'production';
event.server_timestamp = Date.now();
// Filter sensitive data
if (event.user_properties) {
delete event.user_properties.customer_phone;
}
// Add business context
if (event.event === 'dl_purchase') {
event.custom_order_type = 'online';
event.fulfillment_center = 'US-WEST';
}
return event;
}
Advanced Transform Examples
Conditional Field Addition
transformFunction: (event) => {
// Add fields only for specific events
if (event.event === 'dl_purchase') {
event.revenue_category = event.ecommerce?.value > 100 ? 'high' : 'normal';
}
// Add user tier information
if (event.user_properties?.user_id) {
event.user_tier = calculateUserTier(event.user_properties.user_id);
}
return event;
}
Data Validation and Sanitization
transformFunction: (event) => {
// Ensure required fields
if (!event.event_time) {
event.event_time = new Date().toISOString();
}
// Sanitize strings
if (event.user_properties?.session_id) {
event.user_properties.session_id =
event.user_properties.session_id.replace(/[^a-zA-Z0-9_-]/g, '');
}
// Convert currencies
if (event.ecommerce?.currency === 'EUR') {
event.ecommerce.value_usd = event.ecommerce.value * 0.92;
}
return event;
}
PII Redaction
transformFunction: (event) => {
const sensitiveFields = ['email', 'phone', 'ssn', 'credit_card'];
if (event.user_properties) {
sensitiveFields.forEach(field => {
if (event.user_properties[field]) {
delete event.user_properties[field];
}
});
}
return event;
}
Event Sampling
transformFunction: (event) => {
// Sample non-purchase events at 10% to reduce volume
if (event.event !== 'dl_purchase') {
if (Math.random() > 0.1) {
return null; // Drop this event
}
}
return event;
}
Regional Routing
transformFunction: (event) => {
const region = getUserRegion();
event.endpoint_region = region;
// Set processing priority
event.processing_priority = event.event === 'dl_purchase' ? 'high' : 'normal';
// Add regional context
event.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return event;
}
Use Cases
Data Warehouse Integration
Forward events to your data warehouse for long-term analysis and reporting:
endpoint: 'https://api.yourdata.com/ingest/analytics',
headers: {
'Authorization': 'Bearer warehouse-token',
'X-Dataset-ID': 'campaign-cart-events'
},
batchSize: 50, // Larger batches for warehouse efficiency
batchIntervalMs: 10000,
transformFunction: (event) => {
return {
...event,
warehouse_partition: new Date().toISOString().split('T')[0],
ingestion_source: 'campaign-cart'
};
}
Supported: Snowflake, BigQuery, Redshift, Databricks, etc.
Custom Analytics Platform
Send events to your own analytics infrastructure:
endpoint: 'https://analytics.yourcompany.com/api/v1/events',
headers: {
'Authorization': 'Bearer api-key',
'X-App-ID': 'campaign-cart'
},
maxRetries: 3,
transformFunction: (event) => {
return {
...event,
event_version: '2.0',
company_id: 'comp_123',
received_at: new Date().toISOString()
};
}
Marketing Automation
Trigger marketing workflows based on analytics events:
endpoint: 'https://api.marketing-platform.com/webhooks/events',
headers: {
'Authorization': 'Bearer webhook-key',
'X-Integration-ID': 'cart-system'
},
batchSize: 1, // Send events immediately
batchIntervalMs: 100,
transformFunction: (event) => {
// Only send customer conversion events
if (!['dl_purchase', 'dl_view_item'].includes(event.event)) {
return null;
}
return {
...event,
trigger_type: event.event === 'dl_purchase' ? 'purchase' : 'view',
automation_enabled: true
};
}
Platforms: HubSpot, Klaviyo, Braze, Marketo, etc.
Real-time Processing
Process events in real-time systems for immediate action:
endpoint: 'https://streaming.yourcompany.com/events',
headers: {
'Authorization': 'Bearer stream-key'
},
batchSize: 5,
batchIntervalMs: 1000,
transformFunction: (event) => {
return {
...event,
processing_timestamp: Date.now(),
priority: ['dl_purchase', 'dl_error'].includes(event.event) ? 'high' : 'normal'
};
}
Platforms: Kafka, RabbitMQ, Apache Pulsar, AWS Kinesis, etc.
Multi-region Distribution
Send different events to different regional endpoints:
endpoint: 'https://api.yourapp.com/analytics',
transformFunction: (event) => {
// Determine user region
const region = getUserRegion(); // 'us', 'eu', 'apac'
return {
...event,
target_region: region,
regional_endpoint: `https://api-${region}.yourapp.com/analytics`,
processing_priority: event.event === 'dl_purchase' ? 'high' : 'normal',
is_critical_event: ['dl_purchase', 'dl_error'].includes(event.event)
};
}
Example: Multi-endpoint Setup
For advanced scenarios where you need to send events to multiple endpoints or route them based on event type:
import { useAnalytics } from '@campaign-cart/analytics';
const analytics = useAnalytics({
providers: {
custom: {
enabled: true,
settings: {
endpoint: 'https://api.yourapp.com/analytics',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
batchSize: 10,
batchIntervalMs: 5000,
maxRetries: 3,
retryDelayMs: 1000,
// Advanced routing based on event type and attributes
transformFunction: (event) => {
const transformed = {
...event,
app_version: '1.0.0',
environment: process.env.NODE_ENV
};
// Route purchase events to high-priority endpoint
if (event.event === 'dl_purchase') {
transformed.endpoint_priority = 'high';
transformed.endpoint_type = 'purchases';
transformed.requires_acknowledgment = true;
}
// Route behavioral events normally
else if (['dl_view_item', 'dl_add_to_cart', 'dl_remove_from_cart'].includes(event.event)) {
transformed.endpoint_priority = 'normal';
transformed.endpoint_type = 'behaviors';
}
// Route errors to special endpoint
else if (event.event === 'dl_error') {
transformed.endpoint_priority = 'critical';
transformed.endpoint_type = 'errors';
transformed.alert_on_receipt = true;
}
// Add regional information
const userRegion = detectUserRegion();
transformed.region = userRegion;
transformed.regional_endpoint = `https://api-${userRegion}.yourapp.com/analytics`;
return transformed;
}
}
}
}
});
// Helper function to detect user region
function detectUserRegion() {
if (typeof window === 'undefined') return 'default';
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (timezone.includes('America')) return 'us';
if (timezone.includes('Europe')) return 'eu';
if (timezone.includes('Asia') || timezone.includes('Australia')) return 'apac';
return 'default';
}
// In your component
analytics.track('dl_purchase', {
ecommerce: {
value: 99.99,
currency: 'USD',
items: [
{ item_id: 'SKU123', item_name: 'Product', quantity: 1, price: 99.99 }
]
}
});
Troubleshooting
Events Not Being Sent
Problem: Analytics events are not reaching your endpoint.
Solutions:
-
Check endpoint URL: Ensure the
endpointis correct and accessible// Test in browser console
fetch('https://api.yourapp.com/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test: true })
}) -
Verify CORS headers: Ensure your endpoint allows requests from your domain
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization -
Check browser console: Look for network errors (404, 403, CORS errors)
- Open DevTools → Network tab → Filter for your endpoint
- Check Request/Response headers and body
-
Verify configuration: Ensure custom provider is enabled
providers: {
custom: {
enabled: true, // Must be true
settings: { ... }
}
}
401 Unauthorized Errors
Problem: Requests are rejected with 401 status.
Solutions:
-
Check authorization header:
headers: {
'Authorization': 'Bearer YOUR_VALID_TOKEN' // Use actual token
} -
Verify token expiration: Ensure tokens are refreshed periodically
headers: {
'Authorization': `Bearer ${getValidToken()}`
} -
Test endpoint authentication:
curl -X POST https://api.yourapp.com/analytics \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"events": [], "batch_info": {}}'
Retries Not Working
Problem: Failed requests are not being retried.
Solutions:
-
Check retry configuration:
maxRetries: 3, // Must be > 0
retryDelayMs: 1000 // Must be positive -
Check status codes: SDK only retries on 5xx, timeouts, and network errors
- 4xx errors (except some specific cases) are not retried
- 2xx responses are considered success
-
Monitor retry logs: Enable debug logging (if available)
// Check browser DevTools console for messages like:
// "Analytics: Retrying batch (attempt 2 of 3)..."
High Event Loss
Problem: Events are being dropped before reaching your endpoint.
Solutions:
-
Increase batch time limits:
// Give more time before dropping
batchIntervalMs: 10000, // Increased from 5000
maxRetries: 5 // More retry attempts -
Implement page unload handler (SDK should handle this automatically)
-
Check transform function: Ensure it's not filtering out events
transformFunction: (event) => {
console.log('Event:', event); // Debug what's being sent
return event;
}
Transform Function Issues
Problem: Events are being modified incorrectly or transform function has errors.
Solutions:
-
Add error handling:
transformFunction: (event) => {
try {
// Your transform logic
return {
...event,
custom_field: calculateValue(event)
};
} catch (error) {
console.error('Transform error:', error);
return event; // Return original if transform fails
}
} -
Validate return value: Always return an event object or null
transformFunction: (event) => {
// WRONG: return undefined
// CORRECT: return event or null
if (shouldDropEvent(event)) {
return null;
}
return modifiedEvent;
} -
Test transform function independently:
const testEvent = { event: 'dl_view_item', ... };
const result = transformFunction(testEvent);
console.log('Transform result:', result);
Endpoint Timeouts
Problem: Requests are timing out and being retried excessively.
Solutions:
-
Reduce batch size:
batchSize: 5, // Smaller payloads = faster processing -
Increase request timeout (if configurable):
requestTimeoutMs: 10000 // 10 second timeout -
Check backend performance:
# Monitor endpoint response times
curl -w "@curl-format.txt" -o /dev/null -s \
https://api.yourapp.com/analytics -
Verify network connectivity: Check if your server can reach the endpoint
Duplicate Events at Endpoint
Problem: Events appear to be received multiple times.
Solutions:
-
Implement idempotency:
- Use
event_idfield (already unique per event) - Store processed event IDs to detect duplicates
// In your backend
const processedEventIds = new Set();
if (processedEventIds.has(event.event_id)) {
return { success: true }; // Already processed
}
processedEventIds.add(event.event_id);
// Process event... - Use
-
Check batch_info: Events in same batch have unique IDs
const eventIds = batch.events.map(e => e.event_id);
const uniqueIds = new Set(eventIds);
if (uniqueIds.size !== eventIds.length) {
// Handle duplicate detection
} -
Verify retry handling: Ensure retried batches aren't duplicated
- Use batch IDs if implementing retry deduplication
Backend Not Receiving Headers
Problem: Custom headers aren't appearing in requests.
Solutions:
-
Check header configuration:
headers: {
'Authorization': 'Bearer token',
'X-Custom-Header': 'value'
} -
Verify CORS headers (for browser requests):
Access-Control-Allow-Headersmust include custom header names
Access-Control-Allow-Headers: Authorization, Content-Type, X-Custom-Header -
Check header case sensitivity: Some servers normalize headers
// Both work, but be consistent
'Authorization': 'Bearer token'
'authorization': 'Bearer token'
Memory Leaks with Events
Problem: Memory usage increases continuously.
Solutions:
-
Limit batch size:
batchSize: 20, // Don't queue too many
batchIntervalMs: 3000 // Send more frequently -
Implement event sampling in transform function:
transformFunction: (event) => {
// Sample down high-volume events
if (event.event === 'dl_page_view' && Math.random() > 0.1) {
return null; // Drop 90% of page views
}
return event;
} -
Check for event listener leaks: Ensure analytics instance is properly cleaned up
Best Practices
- Always return a 2xx status from your endpoint when events are received
- Make endpoints idempotent using event IDs to handle retries safely
- Log all received events for debugging and auditing
- Monitor endpoint performance to catch issues early
- Use appropriate batch sizes based on your network and server capacity
- Implement rate limiting on your endpoint to prevent abuse
- Validate incoming data to ensure it matches your schema
- Archive analytics data for historical analysis
- Alert on errors (via email, Slack, etc.) for critical endpoints
- Version your endpoint (e.g.,
/api/v1/analytics) for backward compatibility