Skip to main content

Webhooks

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

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

Job Events

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"
}
}
}

Job Failed (job.failed)

Triggered when an asynchronous job encounters an error.

Payload Example:

{
"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"
}
}
}

Training Events

Training Started (training.started)

Triggered when agent training begins.

Payload Example:

{
"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"
}
}
}

Training Completed (training.completed)

Triggered when agent training finishes successfully.

Payload Example:

{
"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%"
}
}
}

Training Failed (training.failed)

Triggered when agent training encounters an error.

Payload Example:

{
"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
}
}
}

Webhook Setup

Configuring Webhook Endpoints

Configure webhook endpoints in your Synthreo workspace:

  1. Navigate to Workspace SettingsWebhooks
  2. Add New Webhook Endpoint
  3. Configure Event Subscriptions

Webhook Configuration:

{
"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"
}
}

Webhook Security

Signature Verification

All webhook payloads are signed using HMAC-SHA256. Verify signatures to ensure requests are from Synthreo:

Header Example:

X-Synthreo-Signature: sha256=a4b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9
X-Synthreo-Delivery: 12345678-1234-5678-9012-123456789012
X-Synthreo-Event: job.completed
X-Synthreo-Timestamp: 1640995200

Signature Verification Example (Node.js):

const 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 handler
app.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');
});

Python Signature Verification:

import hmac
import hashlib
import time
from 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

Implementation Examples

Complete Webhook Handler

Node.js Express Handler:

class 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
}
}

// Usage
const webhookHandler = new SynthreoWebhookHandler(process.env.WEBHOOK_SECRET);

// Add custom event handler
webhookHandler.on('job.completed', async (data, event) => {
// Custom processing logic
console.log('Custom job completion handler');
});

// Express route
app.post('/webhooks/synthreo', express.raw({ type: 'application/json' }),
webhookHandler.handleWebhook.bind(webhookHandler)
);

Webhook vs Polling Comparison

Traditional Polling Approach:

// Inefficient polling approach
async 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');
}

Webhook Approach:

// Efficient webhook approach
class 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);
}

Best Practices

Webhook Endpoint Implementation

Idempotency

Handle duplicate webhook deliveries gracefully:

class 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
}
}

Error Handling and Retries

Implement robust error handling for webhook processing:

class 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.
}
}

Performance Optimization

Async Processing

Process webhooks asynchronously to respond quickly:

class 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;
}
}

Monitoring and Debugging

Webhook Delivery Monitoring

Track webhook delivery success and failures:

class 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`);
});
}
}

Testing Webhooks

Local Development Testing

Use tools like ngrok for local webhook testing:

# Install ngrok
npm install -g ngrok

# Expose local server
ngrok http 3000

# Use the HTTPS URL in your webhook configuration
# Example: https://abc123.ngrok.io/webhooks/synthreo

Webhook Testing Endpoint

Create a test endpoint for debugging:

app.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
});
});

Migration from Polling

Gradual Migration Strategy

Migrate from polling to webhooks gradually:

class 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);
}
}
}

Troubleshooting

Common Issues

Webhook Not Received

Possible causes:

  • Firewall blocking incoming requests
  • SSL certificate issues
  • Incorrect endpoint URL
  • Network connectivity problems

Debug steps:

# Test endpoint accessibility
curl -X POST https://your-app.com/webhooks/synthreo \
-H "Content-Type: application/json" \
-d '{"test": "webhook"}'

# Check SSL certificate
curl -I https://your-app.com/webhooks/synthreo

# Test from different networks
# Use online webhook testing tools

Signature Verification Fails

Common causes:

  • Wrong webhook secret
  • Character encoding issues
  • Clock synchronization problems

Debug approach:

function 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;
}

By 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.