Webhooks
Webhooks allow you to receive real-time notifications when events occur in SymphonyOS, enabling you to build automated workflows and integrations without constantly polling our API.
How Webhooks Work
When you register a webhook, SymphonyOS will send an HTTP POST request to your specified URL whenever a subscribed event occurs. Your endpoint should:
- Respond quickly (within 5 seconds)
- Return a 2xx status code to acknowledge receipt
- Process asynchronously (queue the event for later processing if needed)
- Validate the signature to ensure authenticity
Available Webhook Events
website.published
website.publishedTriggered when: A website's status changes from "draft" to "published", or when a scheduled website is automatically published.
Use cases:
- Send notification to your team when a campaign goes live
- Update your internal dashboard
- Trigger marketing automation workflows
- Log campaign launch in your analytics
Payload:
{
"event": "website.published",
"timestamp": "2025-11-10T14:23:45Z",
"data": {
"websiteId": 456,
"brandId": 123,
"slug": "artist-new-album",
"url": "https://symphony.is/brandSlug/artist-new-album",
"publishedAt": "2025-11-10T14:23:45Z",
"scheduledPublish": false
},
"signature": "sha256=abc123..."
}website.archived
website.archivedTriggered when: A website is archived or deleted.
Use cases:
- Clean up references in your system
- Notify stakeholders that a campaign has ended
- Archive analytics data
- Update campaign status in your database
Payload:
{
"event": "website.archived",
"timestamp": "2025-11-10T14:23:45Z",
"data": {
"websiteId": 456,
"brandId": 123,
"slug": "artist-new-album",
"archivedAt": "2025-11-10T14:23:45Z"
},
"signature": "sha256=abc123..."
}lead.captured
lead.capturedTriggered when: A fan submits their information through a data collector block on any website.
Use cases:
- Add leads to your CRM in real-time
- Send welcome emails immediately
- Trigger drip campaigns
- Update fan segments
- Sync with email marketing platforms (Mailchimp, SendGrid, etc.)
Payload:
{
"event": "lead.captured",
"timestamp": "2025-11-10T14:23:45Z",
"data": {
"websiteId": 456,
"brandId": 123,
"collectDataId": "email-collector-1",
"email": "[email protected]",
"phone": "+14155551234",
"name": "John Doe",
"customFields": {
"city": "Los Angeles",
"favoriteGenre": "Hip Hop"
},
"source": {
"referrer": "https://instagram.com",
"userAgent": "Mozilla/5.0...",
"ipAddress": "192.168.1.1",
"country": "United States"
}
},
"signature": "sha256=abc123..."
}Important notes:
- This event fires immediately when a fan submits the form
- Email addresses are already validated
- Phone numbers are in E.164 format
- PII data is included, so ensure your webhook endpoint is secure (HTTPS only)
presave.completed
presave.completedTriggered when: A fan successfully completes a pre-save action on any platform.
Use cases:
- Track pre-save conversions in real-time
- Send thank-you messages
- Add pre-savers to exclusive fan lists
- Trigger follow-up campaigns
- Calculate pre-save bonuses or rewards
Payload:
{
"event": "presave.completed",
"timestamp": "2025-11-10T14:23:45Z",
"data": {
"websiteId": 991,
"brandId": 123,
"platform": "spotify",
"fanEmail": "[email protected]",
"releaseDate": "2025-12-01T00:00:00Z",
"releaseUrl": "https://open.spotify.com/album/...",
"boosts": {
"followedArtist": true,
"savedCatalog": false,
"emailCollected": true
}
},
"signature": "sha256=abc123..."
}Important notes:
- Fires after the fan successfully authenticates with the streaming platform
fanEmailmay be null if email collection was disabledboostsobject shows which optional actions the fan completed
analytics.threshold
analytics.thresholdTriggered when: A website's analytics metric crosses a predefined threshold.
Use cases:
- Alert your team when a campaign reaches 10k views
- Notify stakeholders of viral growth
- Trigger additional marketing spend
- Celebrate milestones with your team
Payload:
{
"event": "analytics.threshold",
"timestamp": "2025-11-10T14:23:45Z",
"data": {
"websiteId": 456,
"brandId": 123,
"metric": "pageViews",
"value": 10000,
"threshold": 10000,
"thresholdType": "reached"
},
"signature": "sha256=abc123..."
}Supported metrics:
pageViews- Total page viewsuniqueVisitors- Unique visitor counttotalClicks- Total link clickssignups- Total data captures/pre-saves
Note: Thresholds must be configured through your partnership manager or API support.
Registering Webhooks
Step 1: Create Your Webhook Endpoint
Your endpoint must:
- Be accessible via HTTPS (HTTP not allowed for security)
- Respond with status 200-299 within 5 seconds
- Accept POST requests with JSON body
- Validate the signature (recommended)
Example Node.js/Express endpoint:
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
app.post('/webhooks/symphony', (req, res) => {
// Validate signature (see Security section below)
const isValid = validateSignature(req);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Respond immediately
res.status(200).json({ received: true });
// Process asynchronously
processWebhook(req.body).catch(err => {
console.error('Webhook processing error:', err);
});
});
async function processWebhook(payload) {
const { event, timestamp, data } = payload;
switch (event) {
case 'lead.captured':
await addToCRM(data.email, data);
await sendWelcomeEmail(data.email);
break;
case 'presave.completed':
await trackConversion(data);
await sendThankYouMessage(data.fanEmail);
break;
case 'website.published':
await notifyTeam(`Campaign ${data.slug} is live!`);
break;
}
}
app.listen(3000);Step 2: Register the Webhook via API
curl -X POST https://api.symphonyos.co/v2/sym/v2/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/symphony",
"eventType": "lead.captured"
}'Response:
{
"data": {
"id": 12345,
"url": "https://yourapp.com/webhooks/symphony",
"eventType": "lead.captured",
"status": "active",
"createdAt": "2025-11-10T12:00:00Z"
},
"message": "Webhook created successfully",
"isError": false
}Important: You need to register a separate webhook for each event type you want to receive. You can use the same URL for multiple event types.
Step 3: Test Your Webhook
After registration, SymphonyOS will send a test event to your endpoint:
{
"event": "webhook.test",
"timestamp": "2025-11-10T12:00:01Z",
"data": {
"message": "This is a test webhook from SymphonyOS",
"webhookId": 12345
},
"signature": "sha256=abc123..."
}Your endpoint should respond with 200 OK. If we don't receive a successful response, the webhook will be marked as "failing" and you'll receive an email notification.
Webhook Security
Signature Verification
Every webhook includes a signature field containing an HMAC SHA-256 hash. Verify this to ensure the webhook came from SymphonyOS.
Your webhook secret: Provided when you register the webhook (also available in your dashboard or by contacting support).
Node.js example:
import crypto from 'crypto';
function validateSignature(req) {
const signature = req.body.signature;
const webhookSecret = process.env.SYMPHONY_WEBHOOK_SECRET;
// Remove signature from payload for validation
const payload = { ...req.body };
delete payload.signature;
// Create HMAC
const hmac = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(payload))
.digest('hex');
const expectedSignature = `sha256=${hmac}`;
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}Python example:
import hmac
import hashlib
import json
def validate_signature(payload, signature, webhook_secret):
# Remove signature from payload
payload_copy = payload.copy()
payload_copy.pop('signature', None)
# Create HMAC
message = json.dumps(payload_copy, separators=(',', ':')).encode()
hmac_obj = hmac.new(
webhook_secret.encode(),
message,
hashlib.sha256
)
expected_signature = f"sha256={hmac_obj.hexdigest()}"
# Constant-time comparison
return hmac.compare_digest(signature, expected_signature)Webhook Delivery & Retries
Delivery Guarantees
- At-least-once delivery: You may receive the same event multiple times
- Order not guaranteed: Events may arrive out of order
- Use the timestamp field to order events on your end
- Idempotency: Design your webhook handler to be idempotent (safe to process multiple times)
Retry Logic
If your endpoint fails to respond with 2xx:
| Attempt | Delay | Total Time |
|---|---|---|
| 1st retry | 1 minute | 1 min |
| 2nd retry | 5 minutes | 6 min |
| 3rd retry | 15 minutes | 21 min |
| 4th retry | 1 hour | 1h 21min |
| 5th retry | 6 hours | 7h 21min |
After 5 failed attempts, the webhook is marked as "failing" and you'll receive an email alert. You can manually retry from your dashboard or via the API.
Timeout
Webhook requests timeout after 5 seconds. If your processing takes longer, respond immediately with 200 and process asynchronously.
Best Practices
1. Respond Quickly
// Good: Respond immediately, process later
app.post('/webhook', (req, res) => {
res.status(200).json({ received: true });
queue.add('process-webhook', req.body);
});
// Bad: Process before responding
app.post('/webhook', async (req, res) => {
await processWebhook(req.body); // This might timeout!
res.status(200).json({ received: true });
});2. Handle Duplicates
const processedEvents = new Set();
async function processWebhook(payload) {
const eventId = `${payload.event}-${payload.timestamp}-${payload.data.websiteId}`;
if (processedEvents.has(eventId)) {
console.log('Duplicate event, skipping');
return;
}
processedEvents.add(eventId);
// Process event...
}3. Use a Queue System
For production applications, use a queue system (Bull, RabbitMQ, AWS SQS, etc.):
import Queue from 'bull';
const webhookQueue = new Queue('symphony-webhooks', {
redis: process.env.REDIS_URL
});
app.post('/webhooks/symphony', async (req, res) => {
await webhookQueue.add('webhook', req.body, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});
res.status(200).json({ received: true });
});
webhookQueue.process('webhook', async (job) => {
await processWebhook(job.data);
});4. Monitor Webhook Health
Track webhook processing in your monitoring system:
import * as Sentry from '@sentry/node';
async function processWebhook(payload) {
Sentry.setTag('webhook_event', payload.event);
Sentry.setContext('webhook_data', payload.data);
try {
// Process webhook
} catch (error) {
Sentry.captureException(error);
throw error;
}
}5. Log Everything
Keep audit logs of all webhook events:
await db.webhookLogs.create({
event: payload.event,
timestamp: payload.timestamp,
brandId: payload.data.brandId,
websiteId: payload.data.websiteId,
payload: payload,
processedAt: new Date(),
status: 'success'
});Managing Webhooks
List All Registered Webhooks
curl -X GET https://api.symphonyos.co/v2/sym/v2/webhooks \
-H "Authorization: Bearer YOUR_API_KEY"Response:
{
"data": [
{
"id": 12345,
"url": "https://yourapp.com/webhooks/symphony",
"eventType": "lead.captured",
"status": "active",
"createdAt": "2025-11-10T12:00:00Z",
"lastTriggered": "2025-11-10T14:23:45Z",
"lastStatus": "success",
"failureCount": 0
},
{
"id": 12346,
"url": "https://yourapp.com/webhooks/symphony",
"eventType": "presave.completed",
"status": "active",
"createdAt": "2025-11-10T12:01:00Z",
"lastTriggered": "2025-11-10T15:10:22Z",
"lastStatus": "success",
"failureCount": 0
}
],
"message": "Webhooks retrieved successfully",
"isError": false
}Update Webhook URL
curl -X PUT https://api.symphonyos.co/v2/sym/v2/webhooks/12345 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://newdomain.com/webhooks/symphony"
}'Delete Webhook
curl -X DELETE https://api.symphonyos.co/v2/sym/v2/webhooks/12345 \
-H "Authorization: Bearer YOUR_API_KEY"Troubleshooting
Webhook Not Firing
- Check webhook status: Use the list webhooks endpoint to see if it's marked as "failing"
- Verify event occurred: Ensure the event actually happened (e.g., a lead was actually captured)
- Check filters: Some events may have filters configured
- Contact support: Provide the webhook ID and expected timestamp
Webhook Failing
- Check your endpoint: Is it returning 2xx status codes?
- Verify HTTPS: HTTP endpoints are not allowed
- Check timeout: Is your endpoint responding within 5 seconds?
- Review logs: Check your server logs for errors
- Test signature validation: Temporarily disable signature validation to rule out issues
Duplicate Events
This is expected behavior. Design your handlers to be idempotent:
// Use a unique identifier to track processed events
const eventId = `${payload.event}-${payload.data.websiteId}-${payload.timestamp}`;
if (await hasProcessed(eventId)) {
return; // Already processed
}
await markAsProcessed(eventId);Missing Events
- Events are delivered at-least-once, but network issues can cause delays
- Check your webhook logs/dashboard for delivery attempts
- Implement a reconciliation job that polls the API periodically to catch any missed events
Rate Limits
Webhook deliveries do not count against your API rate limits. However, if we detect abuse (e.g., your endpoint is causing errors that trigger excessive retries), we may temporarily disable the webhook and contact you.
Webhook vs Polling
| Method | Use Case | Pros | Cons |
|---|---|---|---|
| Webhooks | Real-time updates, automated workflows | Instant, no polling overhead, scalable | Requires public endpoint, more complex setup |
| Polling | Batch processing, simpler setup | Simple, no endpoint needed | API rate limits, delays, less efficient |
Recommendation: Use webhooks for real-time use cases (lead capture, analytics alerts) and polling for batch operations (daily analytics reports).
Updated about 1 month ago
