Webhooks
Webhooks guide for the Builder API — configure HTTP callbacks to receive real-time notifications when Builder agent workflows complete, fail, or trigger specific events.
Webhooks provide a powerful alternative to polling for receiving real-time notifications about asynchronous operations in the Synthreo Builder API. Instead of repeatedly checking job status or training progress, your application can receive automatic HTTP callbacks when events occur.
Overview
Section titled “Overview”Webhooks are HTTP POST requests sent to your specified endpoints when specific events happen in your Synthreo workspace. This eliminates the need for constant polling and provides immediate notifications for:
- Job completion events (successful or failed)
- Training status changes (started, completed, failed)
- Cognitive diagram execution results
- System notifications and alerts
Webhook Events
Section titled “Webhook Events”Job Events
Section titled “Job Events”Job Completed (job.completed)
Section titled “Job Completed (job.completed)”Triggered when an asynchronous job finishes successfully.
Payload Example:
{ "event": "job.completed", "timestamp": "2024-01-15T10:30:00.000Z", "data": { "job": { "id": "7ea49160-e58a-4fde-a9ed-d442ec0d3820", "diagramId": 12345, "created": "2024-01-15T10:15:00.000Z", "started": "2024-01-15T10:15:02.000Z", "finished": "2024-01-15T10:30:00.000Z", "duration": 898000, "status": "completed" }, "result": { "status": "OK", "outputData": "Processing completed successfully", "errorData": "[]" }, "metadata": { "userInitiated": true, "requestSource": "api", "executionId": "exec-456" } }}```text#### Job Failed (`job.failed`)
Triggered when an asynchronous job encounters an error.
**Payload Example:**
```json{ "event": "job.failed", "timestamp": "2024-01-15T10:25:00.000Z", "data": { "job": { "id": "7ea49160-e58a-4fde-a9ed-d442ec0d3820", "diagramId": 12345, "created": "2024-01-15T10:15:00.000Z", "started": "2024-01-15T10:15:02.000Z", "finished": "2024-01-15T10:25:00.000Z", "duration": 598000, "status": "failed" }, "error": { "code": "EXECUTION_ERROR", "message": "Variable not populated in Azure OpenAI node", "details": { "nodeId": "node-789", "nodeName": "Azure OpenAI", "errorType": "TEMPLATE_VARIABLE_ERROR" } }, "metadata": { "userInitiated": true, "requestSource": "api", "executionId": "exec-456" } }}```text### Training Events
#### Training Started (`training.started`)
Triggered when agent training begins.
**Payload Example:**
```json{ "event": "training.started", "timestamp": "2024-01-15T11:00:00.000Z", "data": { "agent": { "id": 8139, "name": "Customer Support Agent", "previousStateId": 2, "currentStateId": 6, "trainingNodeId": "8bbae8da-b511-4e02-ba9f-e400b04a40a9" }, "training": { "type": "incremental", "repositoryNodeId": 59, "dataSource": "knowledge_base_update", "estimatedDuration": 1800000 }, "metadata": { "initiatedBy": "api", "logText": "Training started by API user" } }}```text#### Training Completed (`training.completed`)
Triggered when agent training finishes successfully.
**Payload Example:**
```json{ "event": "training.completed", "timestamp": "2024-01-15T11:30:00.000Z", "data": { "agent": { "id": 8139, "name": "Customer Support Agent", "previousStateId": 6, "currentStateId": 2, "trainingNodeId": "8bbae8da-b511-4e02-ba9f-e400b04a40a9" }, "training": { "duration": 1798000, "status": "completed", "metrics": { "documentsProcessed": 150, "tokensProcessed": 45000, "modelVersion": "v2.1.3" } }, "metadata": { "completedAt": "2024-01-15T11:30:00.000Z", "performanceImprovement": "12%" } }}```text#### Training Failed (`training.failed`)
Triggered when agent training encounters an error.
**Payload Example:**
```json{ "event": "training.failed", "timestamp": "2024-01-15T11:15:00.000Z", "data": { "agent": { "id": 8139, "name": "Customer Support Agent", "previousStateId": 6, "currentStateId": 3, "trainingNodeId": "8bbae8da-b511-4e02-ba9f-e400b04a40a9" }, "error": { "code": "TRAINING_DATA_ERROR", "message": "Insufficient training data available", "details": { "documentsFound": 5, "minimumRequired": 10, "dataQualityScore": 0.3 } }, "metadata": { "failedAt": "2024-01-15T11:15:00.000Z", "duration": 900000, "retryable": true } }}```text## Webhook Setup
### Configuring Webhook Endpoints
Configure webhook endpoints in your Synthreo workspace:
1. **Navigate to Workspace Settings** → **Webhooks**2. **Add New Webhook Endpoint**3. **Configure Event Subscriptions**
**Webhook Configuration:**
```json{ "url": "https://your-app.com/webhooks/synthreo", "events": [ "job.completed", "job.failed", "training.started", "training.completed", "training.failed" ], "secret": "your-webhook-secret-key", "active": true, "metadata": { "environment": "production", "description": "Main application webhook" }}```text### Webhook Security
#### Signature Verification
All webhook payloads are signed using HMAC-SHA256. Verify signatures to ensure requests are from Synthreo:
**Header Example:**
```textX-Synthreo-Signature: sha256=a4b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9X-Synthreo-Delivery: 12345678-1234-5678-9012-123456789012X-Synthreo-Event: job.completedX-Synthreo-Timestamp: 1640995200```text**Signature Verification Example (Node.js):**
```javascriptconst crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload, 'utf8') .digest('hex');
const providedSignature = signature.replace('sha256=', '');
// Use timing-safe comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(expectedSignature, 'hex'), Buffer.from(providedSignature, 'hex') );}
// Express.js webhook handlerapp.post('/webhooks/synthreo', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-synthreo-signature']; const timestamp = req.headers['x-synthreo-timestamp']; const payload = req.body;
// Verify timestamp (reject old requests) const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - timestamp) > 300) { // 5 minutes tolerance return res.status(400).send('Request timestamp too old'); }
// Verify signature if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); }
// Process webhook const event = JSON.parse(payload); handleWebhookEvent(event);
res.status(200).send('OK');});```text**Python Signature Verification:**
```pythonimport hmacimport hashlibimport timefrom flask import Flask, request, abort
def verify_webhook_signature(payload, signature, secret): expected_signature = hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest()
provided_signature = signature.replace('sha256=', '')
return hmac.compare_digest(expected_signature, provided_signature)
@app.route('/webhooks/synthreo', methods=['POST'])def handle_webhook(): signature = request.headers.get('X-Synthreo-Signature') timestamp = int(request.headers.get('X-Synthreo-Timestamp', 0)) payload = request.get_data()
# Verify timestamp current_time = int(time.time()) if abs(current_time - timestamp) > 300: # 5 minutes tolerance abort(400, 'Request timestamp too old')
# Verify signature if not verify_webhook_signature(payload, signature, os.environ['WEBHOOK_SECRET']): abort(401, 'Invalid signature')
# Process webhook event = request.get_json() handle_webhook_event(event)
return 'OK', 200```text## Implementation Examples
### Complete Webhook Handler
**Node.js Express Handler:**
```javascriptclass SynthreoWebhookHandler { constructor(secret) { this.secret = secret; this.eventHandlers = new Map();
// Register default handlers this.on('job.completed', this.handleJobCompleted.bind(this)); this.on('job.failed', this.handleJobFailed.bind(this)); this.on('training.completed', this.handleTrainingCompleted.bind(this)); this.on('training.failed', this.handleTrainingFailed.bind(this)); }
on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event).push(handler); }
async handleWebhook(req, res) { try { // Verify signature const signature = req.headers['x-synthreo-signature']; const timestamp = req.headers['x-synthreo-timestamp']; const payload = req.body;
if (!this.verifySignature(payload, signature) || !this.verifyTimestamp(timestamp)) { return res.status(401).json({ error: 'Unauthorized' }); }
const event = JSON.parse(payload); await this.processEvent(event);
res.status(200).json({ status: 'processed' });
} catch (error) { console.error('Webhook processing error:', error); res.status(500).json({ error: 'Processing failed' }); } }
async processEvent(event) { const handlers = this.eventHandlers.get(event.event) || [];
console.log(`Processing webhook event: ${event.event}`);
// Execute all handlers for this event type await Promise.allSettled( handlers.map(handler => handler(event.data, event)) ); }
async handleJobCompleted(data, event) { console.log(`Job ${data.job.id} completed in ${data.job.duration}ms`);
// Update database await this.updateJobStatus(data.job.id, 'completed', data.result);
// Notify user await this.notifyUser(data.job.id, 'Your task has completed successfully!');
// Trigger dependent workflows if (data.job.diagramId === 12345) { // Training data processor await this.triggerTrainingWorkflow(data.result); } }
async handleJobFailed(data, event) { console.error(`Job ${data.job.id} failed:`, data.error.message);
// Update database await this.updateJobStatus(data.job.id, 'failed', data.error);
// Send error notification await this.notifyUser(data.job.id, `Task failed: ${data.error.message}`);
// Log for debugging await this.logJobFailure(data.job.id, data.error); }
async handleTrainingCompleted(data, event) { console.log(`Training completed for agent ${data.agent.id}`);
// Update agent status in database await this.updateAgentStatus(data.agent.id, 'ready', data.training.metrics);
// Notify stakeholders await this.notifyTrainingComplete(data.agent.id, data.training.metrics);
// Enable agent for production use await this.enableAgentInProduction(data.agent.id); }
async handleTrainingFailed(data, event) { console.error(`Training failed for agent ${data.agent.id}:`, data.error.message);
// Update agent status await this.updateAgentStatus(data.agent.id, 'training_failed', data.error);
// Check if retryable if (data.metadata.retryable) { console.log('Scheduling training retry...'); await this.scheduleTrainingRetry(data.agent.id, data.error); }
// Alert operations team await this.alertOperationsTeam(data.agent.id, data.error); }
verifySignature(payload, signature) { const expectedSignature = require('crypto') .createHmac('sha256', this.secret) .update(payload, 'utf8') .digest('hex');
const providedSignature = signature.replace('sha256=', '');
return require('crypto').timingSafeEqual( Buffer.from(expectedSignature, 'hex'), Buffer.from(providedSignature, 'hex') ); }
verifyTimestamp(timestamp) { const currentTime = Math.floor(Date.now() / 1000); return Math.abs(currentTime - timestamp) <= 300; // 5 minutes }}
// Usageconst webhookHandler = new SynthreoWebhookHandler(process.env.WEBHOOK_SECRET);
// Add custom event handlerwebhookHandler.on('job.completed', async (data, event) => { // Custom processing logic console.log('Custom job completion handler');});
// Express routeapp.post('/webhooks/synthreo', express.raw({ type: 'application/json' }), webhookHandler.handleWebhook.bind(webhookHandler));```text### Webhook vs Polling Comparison
**Traditional Polling Approach:**
```javascript// Inefficient polling approachasync function pollJobStatus(jobId) { const startTime = Date.now(); const timeout = 3600000; // 1 hour
while (Date.now() - startTime < timeout) { try { const response = await checkJobStatus(jobId);
if (response.status === 200) { console.log('Job completed!'); return response.data; }
// Wait 30 seconds before next poll await new Promise(resolve => setTimeout(resolve, 30000));
} catch (error) { console.error('Polling error:', error); await new Promise(resolve => setTimeout(resolve, 60000)); } }
throw new Error('Job polling timed out');}```text**Webhook Approach:**
```javascript// Efficient webhook approachclass JobManager { constructor() { this.activeJobs = new Map(); this.webhookHandler = new SynthreoWebhookHandler(process.env.WEBHOOK_SECRET);
// Register job completion handler this.webhookHandler.on('job.completed', this.onJobCompleted.bind(this)); this.webhookHandler.on('job.failed', this.onJobFailed.bind(this)); }
async startJob(diagramId, payload) { const response = await this.executeAsJob(diagramId, payload); const jobId = response.job.id;
// Store job promise for later resolution return new Promise((resolve, reject) => { this.activeJobs.set(jobId, { resolve, reject, startTime: Date.now() }); }); }
onJobCompleted(data, event) { const jobId = data.job.id; const jobPromise = this.activeJobs.get(jobId);
if (jobPromise) { jobPromise.resolve(data.result); this.activeJobs.delete(jobId); } }
onJobFailed(data, event) { const jobId = data.job.id; const jobPromise = this.activeJobs.get(jobId);
if (jobPromise) { jobPromise.reject(new Error(data.error.message)); this.activeJobs.delete(jobId); } }}
// Usage: No polling required!const jobManager = new JobManager();try { const result = await jobManager.startJob(12345, { userSays: "process data" }); console.log('Job completed:', result);} catch (error) { console.error('Job failed:', error);}```text## Best Practices
### Webhook Endpoint Implementation
#### Idempotency
Handle duplicate webhook deliveries gracefully:
```javascriptclass IdempotentWebhookHandler { constructor() { this.processedEvents = new Set(); this.cleanupInterval = 3600000; // 1 hour
// Cleanup old event IDs periodically setInterval(() => this.cleanup(), this.cleanupInterval); }
async processWebhook(event) { const eventId = event.metadata?.deliveryId || `${event.event}-${event.timestamp}`;
// Check if already processed if (this.processedEvents.has(eventId)) { console.log(`Duplicate webhook ignored: ${eventId}`); return { status: 'duplicate', eventId }; }
try { // Process the event await this.handleEvent(event);
// Mark as processed this.processedEvents.add(eventId);
return { status: 'processed', eventId };
} catch (error) { console.error(`Webhook processing failed: ${eventId}`, error); throw error; } }
cleanup() { // Keep only recent event IDs (last hour) const oneHourAgo = Date.now() - 3600000; this.processedEvents.clear(); // Simple approach - clear all periodically }}```text#### Error Handling and Retries
Implement robust error handling for webhook processing:
```javascriptclass RobustWebhookHandler { constructor() { this.retryDelays = [1000, 5000, 15000, 30000]; // Exponential backoff this.maxRetries = 3; }
async processWebhookWithRetry(event) { let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { await this.processWebhook(event);
if (attempt > 0) { console.log(`Webhook processed successfully after ${attempt} retries`); }
return { status: 'success', attempts: attempt + 1 };
} catch (error) { lastError = error;
if (attempt < this.maxRetries) { const delay = this.retryDelays[attempt] || 30000; console.log(`Webhook processing failed, retrying in ${delay}ms:`, error.message); await new Promise(resolve => setTimeout(resolve, delay)); } } }
// All retries failed console.error('Webhook processing failed after all retries:', lastError); await this.sendToDeadLetterQueue(event, lastError); throw lastError; }
async sendToDeadLetterQueue(event, error) { // Log failed events for manual review console.error('DEAD LETTER QUEUE:', { event: event.event, timestamp: event.timestamp, error: error.message, data: event.data });
// Could send to external queue service, database, etc. }}```text### Performance Optimization
#### Async Processing
Process webhooks asynchronously to respond quickly:
```javascriptclass AsyncWebhookHandler { constructor() { this.processingQueue = []; this.isProcessing = false; }
async handleWebhook(req, res) { try { // Verify signature quickly if (!this.verifySignature(req)) { return res.status(401).send('Unauthorized'); }
const event = JSON.parse(req.body);
// Add to processing queue this.processingQueue.push(event);
// Respond immediately res.status(200).send('Accepted');
// Process asynchronously this.processQueue();
} catch (error) { console.error('Webhook handling error:', error); res.status(500).send('Error'); } }
async processQueue() { if (this.isProcessing || this.processingQueue.length === 0) { return; }
this.isProcessing = true;
while (this.processingQueue.length > 0) { const event = this.processingQueue.shift();
try { await this.processEvent(event); } catch (error) { console.error('Queue processing error:', error); // Could implement retry logic here } }
this.isProcessing = false; }}```text## Monitoring and Debugging
### Webhook Delivery Monitoring
Track webhook delivery success and failures:
```javascriptclass WebhookMonitor { constructor() { this.metrics = { totalReceived: 0, successfullyProcessed: 0, failed: 0, duplicates: 0, byEventType: new Map(), responseTimeSum: 0 }; }
recordWebhook(event, status, processingTime) { this.metrics.totalReceived++; this.metrics.responseTimeSum += processingTime;
switch (status) { case 'success': this.metrics.successfullyProcessed++; break; case 'failed': this.metrics.failed++; break; case 'duplicate': this.metrics.duplicates++; break; }
// Track by event type const eventType = event.event; const typeStats = this.metrics.byEventType.get(eventType) || { count: 0, success: 0, failed: 0 }; typeStats.count++; if (status === 'success') typeStats.success++; if (status === 'failed') typeStats.failed++; this.metrics.byEventType.set(eventType, typeStats); }
getReport() { const avgResponseTime = this.metrics.responseTimeSum / this.metrics.totalReceived; const successRate = (this.metrics.successfullyProcessed / this.metrics.totalReceived) * 100;
console.log('=== Webhook Monitoring Report ==='); console.log(`Total Received: ${this.metrics.totalReceived}`); console.log(`Successfully Processed: ${this.metrics.successfullyProcessed} (${successRate.toFixed(1)}%)`); console.log(`Failed: ${this.metrics.failed}`); console.log(`Duplicates: ${this.metrics.duplicates}`); console.log(`Avg Response Time: ${avgResponseTime.toFixed(2)}ms`);
console.log('\nBy Event Type:'); this.metrics.byEventType.forEach((stats, eventType) => { const eventSuccessRate = (stats.success / stats.count) * 100; console.log(` ${eventType}: ${stats.count} total, ${eventSuccessRate.toFixed(1)}% success`); }); }}```text### Testing Webhooks
#### Local Development Testing
Use tools like ngrok for local webhook testing:
```bash
npm install -g ngrok
ngrok http 3000
```text#### Webhook Testing Endpoint
Create a test endpoint for debugging:
```javascriptapp.post('/webhooks/test', (req, res) => { console.log('=== Webhook Test Received ==='); console.log('Headers:', req.headers); console.log('Body:', JSON.stringify(req.body, null, 2)); console.log('=============================');
res.status(200).json({ status: 'received', timestamp: new Date().toISOString(), bodySize: JSON.stringify(req.body).length });});```text## Migration from Polling
### Gradual Migration Strategy
Migrate from polling to webhooks gradually:
```javascriptclass HybridJobManager { constructor(options = {}) { this.useWebhooks = options.useWebhooks || false; this.webhookTimeout = options.webhookTimeout || 300000; // 5 minutes this.activeJobs = new Map(); }
async executeJob(diagramId, payload) { const response = await this.executeAsJob(diagramId, payload); const jobId = response.job.id;
if (this.useWebhooks) { return this.waitForWebhook(jobId); } else { return this.pollJobStatus(jobId); } }
async waitForWebhook(jobId) { return new Promise((resolve, reject) => { // Set up webhook listener this.activeJobs.set(jobId, { resolve, reject });
// Fallback to polling if webhook doesn't arrive setTimeout(() => { if (this.activeJobs.has(jobId)) { console.log(`Webhook timeout for job ${jobId}, falling back to polling`); this.activeJobs.delete(jobId); this.pollJobStatus(jobId).then(resolve).catch(reject); } }, this.webhookTimeout); }); }
onWebhookReceived(jobId, result) { const jobPromise = this.activeJobs.get(jobId); if (jobPromise) { jobPromise.resolve(result); this.activeJobs.delete(jobId); } }}```text## Troubleshooting
### Common Issues
#### Webhook Not Received
**Possible causes:**
- Firewall blocking incoming requests- SSL certificate issues- Incorrect endpoint URL- Network connectivity problems
**Debug steps:**
```bash
curl -X POST https://your-app.com/webhooks/synthreo \ -H "Content-Type: application/json" \ -d '{"test": "webhook"}'
curl -I https://your-app.com/webhooks/synthreo
```text#### Signature Verification Fails
**Common causes:**
- Wrong webhook secret- Character encoding issues- Clock synchronization problems
**Debug approach:**
```javascriptfunction debugSignature(payload, signature, secret) { console.log('Payload length:', payload.length); console.log('Payload (first 100 chars):', payload.toString().substring(0, 100)); console.log('Provided signature:', signature);
const expectedSignature = crypto .createHmac('sha256', secret) .update(payload, 'utf8') .digest('hex');
console.log('Expected signature:', expectedSignature); console.log('Secret length:', secret.length);
return signature.replace('sha256=', '') === expectedSignature;}```textBy implementing webhooks, you can build more efficient, responsive applications that react immediately to events in your Synthreo workspace, eliminating the overhead and latency of polling-based approaches.