Webhooks
Receive real-time notifications when events occur in Exepron. Webhooks enable your application to react immediately to project changes, task updates, and critical chain events.
On this page
What are Webhooks?
Webhooks are HTTP callbacks that Exepron sends to your application when specific events occur. Instead of polling the API for changes, webhooks push data to your endpoint in real-time, enabling:
- Real-time Updates: Instant notifications when projects or tasks change
- Efficient Integration: No need for constant polling
- Event-Driven Architecture: Build reactive applications
- Reduced API Calls: Save on rate limits and bandwidth
- Automatic Workflows: Trigger actions based on project events
How Webhooks Work
A project starts, task completes, or buffer threshold is exceeded
Exepron detects the event and prepares the webhook payload
Payload is sent to your registered endpoint URL
Your application receives and processes the event data
Return HTTP 200 OK to confirm successful processing
Supported Events
Exepron supports webhooks for 10 critical project management events:
1. Project Started
Event: project.started
Triggered when: A project transitions from Planning to Active status
Use cases:
- Notify team members
- Initialize tracking systems
- Start time tracking
2. Task Turned Red
Event: task.red
Triggered when: A task's buffer consumption exceeds critical threshold (>90%)
Use cases:
- Alert project managers
- Escalate to stakeholders
- Trigger recovery plans
3. Project Frozen
Event: project.frozen
Triggered when: A project status changes to Frozen/On Hold
Use cases:
- Pause billing
- Reallocate resources
- Update dashboards
4. Task Available to Start
Event: task.ready
Triggered when: All predecessor tasks complete, making task available
Use cases:
- Notify assigned resources
- Update work queues
- Schedule resources
5. Task Finished
Event: task.completed
Triggered when: A task reaches 100% completion
Use cases:
- Update progress reports
- Trigger dependent workflows
- Calculate metrics
6. Milestone Achieved
Event: milestone.achieved
Triggered when: A project milestone is marked as complete
Use cases:
- Send achievement notifications
- Trigger payments
- Generate reports
7. Task Status Changed
Event: task.status_changed
Triggered when: Any task status transition occurs
Use cases:
- Audit trail
- Update external systems
- Track workflow states
8. Task Duration Changed
Event: task.duration_changed
Triggered when: Remaining duration is updated
Use cases:
- Recalculate schedules
- Update forecasts
- Adjust resource allocation
9. Project Early Warning
Event: project.early_warning
Triggered when: Buffer consumption enters yellow zone (50-70%)
Use cases:
- Preventive notifications
- Risk assessment triggers
- Management alerts
10. Project Status Changed
Event: project.status_changed
Triggered when: Project status transitions (Planning → Active → Complete)
Use cases:
- Lifecycle tracking
- Portfolio updates
- Compliance reporting
Setting Up Webhooks
Step 1: Create Your Webhook Endpoint
First, create an HTTP endpoint in your application that can receive POST requests:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/exepron', (req, res) => {
// Verify webhook signature
const signature = req.headers['x-exepron-signature'];
const secret = process.env.WEBHOOK_SECRET;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const { event, data } = req.body;
switch(event) {
case 'project.started':
handleProjectStarted(data);
break;
case 'task.red':
handleTaskRed(data);
break;
// ... handle other events
}
// Always return 200 OK quickly
res.status(200).send('OK');
// Process async operations after responding
processWebhookAsync(event, data);
});
app.listen(3000, () => {
console.log('Webhook endpoint listening on port 3000');
});
[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
private readonly ILogger _logger;
private readonly IWebhookProcessor _processor;
[HttpPost("exepron")]
public async Task HandleExepronWebhook(
[FromBody] WebhookPayload payload,
[FromHeader(Name = "X-Exepron-Signature")] string signature)
{
// Verify signature
if (!VerifySignature(payload, signature))
{
return Unauthorized("Invalid signature");
}
// Log the event
_logger.LogInformation($"Received webhook: {payload.Event}");
// Queue for async processing
await _processor.QueueWebhook(payload);
// Return immediately
return Ok();
}
private bool VerifySignature(WebhookPayload payload, string signature)
{
var secret = Configuration["Webhooks:Secret"];
var json = JsonSerializer.Serialize(payload);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(json));
var computedSignature = Convert.ToHexString(computedHash).ToLower();
return computedSignature == signature;
}
}
public class WebhookPayload
{
public string Event { get; set; }
public DateTime Timestamp { get; set; }
public dynamic Data { get; set; }
public string WebhookId { get; set; }
}
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
@app.route('/webhooks/exepron', methods=['POST'])
def handle_webhook():
# Get the signature from headers
signature = request.headers.get('X-Exepron-Signature')
# Verify signature
secret = os.environ.get('WEBHOOK_SECRET')
payload = request.get_data()
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
if signature != expected_signature:
return jsonify({'error': 'Invalid signature'}), 401
# Parse the webhook data
data = request.get_json()
event = data['event']
event_data = data['data']
# Handle the event
if event == 'project.started':
handle_project_started(event_data)
elif event == 'task.red':
handle_task_red(event_data)
# ... handle other events
# Return success immediately
return jsonify({'status': 'success'}), 200
def handle_project_started(data):
print(f"Project {data['projectName']} has started")
# Your business logic here
def handle_task_red(data):
print(f"Task {data['taskName']} is in red zone!")
# Send alerts, notifications, etc.
if __name__ == '__main__':
app.run(port=3000)
Step 2: Register Your Webhook
Register your endpoint with Exepron using the API:
POST /api/v1/webhooks
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"name": "Production Webhook",
"url": "https://api.yourapp.com/webhooks/exepron",
"events": [
"project.started",
"task.red",
"milestone.achieved"
],
"secret": "your-webhook-secret-key",
"isActive": true,
"headers": {
"X-Custom-Header": "value"
}
}
Step 3: Test Your Webhook
Send a test event to verify your endpoint is working:
POST /api/v1/webhooks/{webhookId}/test
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"event": "project.started"
}
Webhook Payloads
All webhook payloads follow a consistent structure with event-specific data:
Base Payload Structure
{
"webhookId": "wh_abc123def456",
"event": "project.started",
"timestamp": "2025-01-30T14:30:00Z",
"accountId": 12345,
"data": {
// Event-specific data
},
"metadata": {
"version": "1.0",
"retryCount": 0
}
}
Event-Specific Payloads
Project Started
{
"event": "project.started",
"data": {
"projectId": 789,
"projectName": "Q1 Product Launch",
"startDate": "2025-02-01",
"targetEndDate": "2025-03-31",
"managerId": 456,
"managerName": "Jane Smith",
"managerEmail": "jane.smith@company.com",
"bufferSize": 15,
"criticalChainLength": 45,
"teamSize": 8
}
}
Task Turned Red
{
"event": "task.red",
"data": {
"taskId": 234,
"taskName": "Backend Development",
"projectId": 789,
"projectName": "Q1 Product Launch",
"assignedTo": {
"id": 567,
"name": "John Developer",
"email": "john.dev@company.com"
},
"bufferConsumption": 92.5,
"remainingDuration": 3,
"dueDate": "2025-02-15",
"previousStatus": "yellow",
"criticalChainImpact": true
}
}
Milestone Achieved
{
"event": "milestone.achieved",
"data": {
"milestoneId": 345,
"milestoneName": "Alpha Release",
"projectId": 789,
"projectName": "Q1 Product Launch",
"achievedDate": "2025-02-10",
"plannedDate": "2025-02-12",
"daysEarly": 2,
"completedBy": {
"id": 678,
"name": "Sarah Manager",
"email": "sarah.manager@company.com"
},
"linkedDeliverables": [
{
"id": 901,
"name": "Alpha Build",
"status": "Delivered"
}
]
}
}
Task Status Changed
{
"event": "task.status_changed",
"data": {
"taskId": 234,
"taskName": "Frontend Implementation",
"projectId": 789,
"previousStatus": "NotStarted",
"newStatus": "InProgress",
"changedBy": {
"id": 123,
"name": "Mike Developer",
"email": "mike@company.com"
},
"changedAt": "2025-01-30T10:15:00Z",
"percentComplete": 0,
"estimatedCompletion": "2025-02-20"
}
}
Project Early Warning
{
"event": "project.early_warning",
"data": {
"projectId": 789,
"projectName": "Q1 Product Launch",
"warningLevel": "yellow",
"bufferConsumption": 65,
"projectedCompletion": "2025-04-05",
"originalTarget": "2025-03-31",
"daysLate": 5,
"criticalTasks": [
{
"id": 234,
"name": "Backend Development",
"bufferConsumption": 78
}
],
"recommendations": [
"Consider adding resources to critical tasks",
"Review and reduce scope if possible"
]
}
}
Security & Verification
Webhook Signatures
Every webhook request includes a signature in the X-Exepron-Signature header. Always verify this signature to ensure the request came from Exepron:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage in your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-exepron-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).send('Unauthorized');
}
// Process valid webhook
// ...
});
using System.Security.Cryptography;
using System.Text;
public bool VerifyWebhookSignature(object payload, string signature, string secret)
{
var json = JsonSerializer.Serialize(payload);
var secretBytes = Encoding.UTF8.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(json);
using var hmac = new HMACSHA256(secretBytes);
var computedHash = hmac.ComputeHash(payloadBytes);
var computedSignature = Convert.ToHexString(computedHash).ToLower();
// Timing-safe comparison
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(computedSignature),
Encoding.UTF8.GetBytes(signature)
);
}
import hmac
import hashlib
import json
def verify_webhook_signature(payload, signature, secret):
"""Verify the webhook signature using HMAC-SHA256"""
# Compute expected signature
expected_signature = hmac.new(
secret.encode('utf-8'),
json.dumps(payload).encode('utf-8'),
hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(signature, expected_signature)
# Usage in Flask
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Exepron-Signature')
secret = os.environ.get('WEBHOOK_SECRET')
payload = request.get_json()
if not verify_webhook_signature(payload, signature, secret):
abort(401)
# Process valid webhook
# ...
Security Best Practices
- Always verify signatures - Never process webhooks without verification
- Use HTTPS endpoints - Ensure your webhook URL uses SSL/TLS
- Implement idempotency - Handle duplicate webhooks gracefully
- Validate payload structure - Check required fields exist
- Rate limit your endpoint - Protect against abuse
- Log all webhook activity - Maintain audit trail
- Rotate secrets regularly - Update webhook secrets periodically
- IP whitelisting - Restrict access to Exepron IP ranges (optional)
- Production:
52.14.0.0/16 - Sandbox:
54.23.0.0/16
Testing Webhooks
Local Testing with ngrok
Use ngrok to expose your local webhook endpoint for testing:
# Install ngrok
npm install -g ngrok
# Start your local server
node webhook-server.js
# In another terminal, expose your local port
ngrok http 3000
# Use the ngrok URL for webhook registration
https://abc123.ngrok.io/webhooks/exepron
Test Event Generator
Trigger test events from the API:
POST /api/v1/webhooks/{webhookId}/test
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"event": "task.red",
"data": {
"taskId": 123,
"taskName": "Test Task",
"bufferConsumption": 95
}
}
Webhook Testing Tool
// Simple webhook tester
const axios = require('axios');
class WebhookTester {
constructor(webhookUrl, secret) {
this.webhookUrl = webhookUrl;
this.secret = secret;
}
async sendTestEvent(event, data) {
const payload = {
webhookId: 'test_' + Date.now(),
event: event,
timestamp: new Date().toISOString(),
accountId: 12345,
data: data,
metadata: {
version: '1.0',
retryCount: 0
}
};
const signature = this.generateSignature(payload);
try {
const response = await axios.post(this.webhookUrl, payload, {
headers: {
'Content-Type': 'application/json',
'X-Exepron-Signature': signature
}
});
console.log('Webhook test successful:', response.status);
return response;
} catch (error) {
console.error('Webhook test failed:', error.message);
throw error;
}
}
generateSignature(payload) {
const crypto = require('crypto');
return crypto
.createHmac('sha256', this.secret)
.update(JSON.stringify(payload))
.digest('hex');
}
}
// Usage
const tester = new WebhookTester(
'http://localhost:3000/webhooks/exepron',
'your-secret'
);
tester.sendTestEvent('project.started', {
projectId: 789,
projectName: 'Test Project'
});
Debugging Tips
- Check webhook logs in Exepron dashboard
- Verify signature calculation matches
- Ensure payload parsing is correct
- Test with minimal payload first
- Check for firewall/proxy issues
- Validate SSL certificates
Webhook Handler Examples
Slack Notification Handler
async function handleTaskRed(data) {
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
const message = {
text: `🚨 Task Alert: ${data.taskName} is in RED zone!`,
attachments: [{
color: 'danger',
fields: [
{
title: 'Project',
value: data.projectName,
short: true
},
{
title: 'Buffer Consumption',
value: `${data.bufferConsumption}%`,
short: true
},
{
title: 'Assigned To',
value: data.assignedTo.name,
short: true
},
{
title: 'Due Date',
value: data.dueDate,
short: true
}
],
actions: [
{
type: 'button',
text: 'View Task',
url: `https://app.exepron.com/tasks/${data.taskId}`
}
]
}]
};
await axios.post(slackWebhookUrl, message);
}
Email Notification Handler
const nodemailer = require('nodemailer');
async function handleMilestoneAchieved(data) {
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
const mailOptions = {
from: 'notifications@yourcompany.com',
to: 'stakeholders@yourcompany.com',
subject: `✅ Milestone Achieved: ${data.milestoneName}`,
html: `
Milestone Achievement
${data.milestoneName} has been achieved!
Project:
${data.projectName}
Achieved Date:
${data.achievedDate}
Days Early:
${data.daysEarly}
`
};
await transporter.sendMail(mailOptions);
}
Database Update Handler
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function handleProjectStatusChanged(data) {
// Update local database
await prisma.projectSync.upsert({
where: { exepronId: data.projectId },
update: {
status: data.newStatus,
lastModified: new Date(data.changedAt)
},
create: {
exepronId: data.projectId,
name: data.projectName,
status: data.newStatus,
lastModified: new Date(data.changedAt)
}
});
// Log the change
await prisma.auditLog.create({
data: {
entityType: 'project',
entityId: data.projectId,
action: 'status_changed',
oldValue: data.previousStatus,
newValue: data.newStatus,
changedBy: data.changedBy.name,
changedAt: new Date(data.changedAt)
}
});
}
Managing Webhooks via API
List All Webhooks
GET /api/v1/webhooks
Authorization: Bearer YOUR_ACCESS_TOKEN
Response:
{
"value": [
{
"id": "wh_123",
"name": "Production Webhook",
"url": "https://api.yourapp.com/webhooks",
"events": ["project.started", "task.red"],
"isActive": true,
"createdAt": "2025-01-15T10:00:00Z",
"lastTriggered": "2025-01-30T14:30:00Z",
"statistics": {
"totalCalls": 1543,
"successfulCalls": 1521,
"failedCalls": 22
}
}
]
}
Update Webhook
PUT /api/v1/webhooks/{webhookId}
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"name": "Updated Webhook Name",
"events": [
"project.started",
"task.red",
"milestone.achieved",
"project.early_warning"
],
"isActive": true
}
Delete Webhook
DELETE /api/v1/webhooks/{webhookId}
Authorization: Bearer YOUR_ACCESS_TOKEN
Get Webhook Logs
GET /api/v1/webhooks/{webhookId}/logs?$top=50
Authorization: Bearer YOUR_ACCESS_TOKEN
Response:
{
"value": [
{
"id": "log_456",
"webhookId": "wh_123",
"event": "task.red",
"timestamp": "2025-01-30T14:30:00Z",
"status": "success",
"httpStatus": 200,
"responseTime": 145,
"retries": 0
},
{
"id": "log_457",
"webhookId": "wh_123",
"event": "project.started",
"timestamp": "2025-01-30T14:35:00Z",
"status": "failed",
"httpStatus": 500,
"error": "Internal Server Error",
"retries": 3
}
]
}
Pause/Resume Webhook
# Pause
PATCH /api/v1/webhooks/{webhookId}/pause
Authorization: Bearer YOUR_ACCESS_TOKEN
# Resume
PATCH /api/v1/webhooks/{webhookId}/resume
Authorization: Bearer YOUR_ACCESS_TOKEN
Retries & Failure Handling
Retry Policy
Exepron implements an exponential backoff retry strategy for failed webhooks:
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0 seconds |
| 2 | 10 seconds | 10 seconds |
| 3 | 1 minute | 1 min 10 sec |
| 4 | 5 minutes | 6 min 10 sec |
| 5 | 30 minutes | 36 min 10 sec |
| 6 | 2 hours | 2 hr 36 min |
Failure Conditions
Webhooks are retried when:
- HTTP status code is 5xx (server errors)
- Connection timeout (30 seconds)
- DNS resolution failure
- SSL certificate errors
Webhooks are NOT retried when:
- HTTP status code is 4xx (client errors)
- Response is 200 OK
- Maximum retries reached (6 attempts)
Handling Failures in Your Application
app.post('/webhook', async (req, res) => {
try {
// Quick validation
if (!req.body.event) {
// 400 Bad Request - won't be retried
return res.status(400).send('Missing event');
}
// Return 200 immediately
res.status(200).send('OK');
// Process async
await processWebhookAsync(req.body);
} catch (error) {
// Log error but already returned 200
console.error('Webhook processing error:', error);
// Store failed webhook for manual processing
await storeFailedWebhook(req.body, error);
}
});
async function processWebhookAsync(payload) {
// Queue for background processing
await queue.add('webhook-processing', {
payload,
receivedAt: new Date()
}, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
}
Webhook Delivery Guarantees
- At-least-once delivery: Webhooks may be sent multiple times
- Order not guaranteed: Events may arrive out of sequence
- Idempotency required: Handle duplicate events gracefully
Best Practices
1. Respond Quickly
Return 200 OK within 5 seconds, process asynchronously:
// Good
app.post('/webhook', (req, res) => {
res.status(200).send();
processAsync(req.body);
});
// Bad
app.post('/webhook', async (req, res) => {
await longRunningProcess(req.body);
res.status(200).send();
});
2. Implement Idempotency
Use webhook ID to prevent duplicate processing:
const processedWebhooks = new Set();
function processWebhook(payload) {
if (processedWebhooks.has(payload.webhookId)) {
console.log('Duplicate webhook, skipping');
return;
}
processedWebhooks.add(payload.webhookId);
// Process webhook...
}
3. Queue for Reliability
Use message queues for robust processing:
const Queue = require('bull');
const webhookQueue = new Queue('webhooks');
app.post('/webhook', (req, res) => {
webhookQueue.add(req.body);
res.status(200).send();
});
webhookQueue.process(async (job) => {
await handleWebhook(job.data);
});
4. Monitor & Alert
Track webhook metrics and failures:
const metrics = {
received: 0,
processed: 0,
failed: 0
};
function recordWebhook(status) {
metrics.received++;
if (status === 'success') metrics.processed++;
if (status === 'failed') metrics.failed++;
// Alert if failure rate is high
const failureRate = metrics.failed / metrics.received;
if (failureRate > 0.05) {
alertTeam('High webhook failure rate');
}
}
5. Validate & Sanitize
Always validate webhook data:
const Joi = require('joi');
const webhookSchema = Joi.object({
event: Joi.string().required(),
data: Joi.object().required(),
timestamp: Joi.date().iso().required()
});
function validateWebhook(payload) {
const { error } = webhookSchema.validate(payload);
if (error) {
throw new Error(`Invalid webhook: ${error.message}`);
}
}
6. Graceful Degradation
Handle webhook failures without breaking your app:
async function syncWithExepron() {
try {
// Primary: Use webhooks
await processRecentWebhooks();
} catch (error) {
// Fallback: Poll API
console.log('Webhook processing failed, polling API');
await pollExepronAPI();
}
}