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.

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

1
Event Occurs

A project starts, task completes, or buffer threshold is exceeded

2
Webhook Triggered

Exepron detects the event and prepares the webhook payload

3
HTTP POST Request

Payload is sent to your registered endpoint URL

4
Your Handler Processes

Your application receives and processes the event data

5
Acknowledge Receipt

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)
Exepron IP Ranges: Webhooks originate from these IP ranges:
  • 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}

View Project

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

Additional Resources