Skip to content

Webhooks

Webhooks enable MFTPlus to push real-time notifications to your application when important events occur. Instead of polling the API for status updates, configure webhooks to receive HTTP callbacks as events happen.

Overview

When an event occurs in your MFTPlus deployment (such as a transfer completing or an agent going offline), MFTPlus sends an HTTP POST request to a URL you configure. Your application can then process these events to trigger workflows, update databases, or send notifications.

Use Cases

  • Transfer Notifications: Alert teams when file transfers complete or fail
  • Agent Monitoring: Trigger alerts when agents go offline or certificates expire
  • Audit Integration: Stream events to SIEM systems for compliance
  • Automation: Chain additional workflows based on transfer status
  • Custom Notifications: Send alerts to Slack, Microsoft Teams, or other services

Configuration

Configure webhooks through the MFTPlus admin panel or API.

Admin Panel Configuration

  1. Navigate to SettingsWebhooks
  2. Click Add Webhook
  3. Configure webhook properties:
    • Name: Descriptive label for this webhook
    • URL: Endpoint that will receive POST requests
    • Events: Select which events trigger this webhook
    • Secret: (Optional) HMAC signature key for security
    • Active: Enable/disable the webhook

API Configuration

Create a webhook via the REST API:

bash
curl -X POST https://api.mftplus.co.za/api/webhooks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "name": "Transfer Alerts",
    "url": "https://your-app.com/webhooks/mftplus",
    "events": ["transfer.completed", "transfer.failed"],
    "secret": "your-signing-secret",
    "active": true
  }'

Event Types

MFTPlus sends webhook notifications for the following events:

Transfer Events

EventDescriptionTrigger
transfer.createdA new transfer was initiatedTransfer job starts
transfer.progressTransfer progress updateEvery 10% progress
transfer.completedTransfer completed successfullyFile transfer finishes
transfer.failedTransfer failedError during transfer
transfer.cancelledTransfer was cancelledManual or automatic cancellation

Agent Events

EventDescriptionTrigger
agent.registeredNew agent registeredAgent joins deployment
agent.connectedAgent came onlineAgent connects after offline
agent.disconnectedAgent went offlineAgent loses connection
agent.certificate.expiringCertificate expiring soon30 days before expiration
agent.certificate.expiredCertificate expiredCertificate no longer valid

Job Events

EventDescriptionTrigger
job.startedScheduled job startedJob execution begins
job.completedJob completed successfullyAll transfers in job finish
job.failedJob failedOne or more transfers fail
job.skippedJob skippedSchedule conditions not met

System Events

EventDescriptionTrigger
system.health.degradedSystem performance degradedResource thresholds exceeded
system.storage.warningStorage capacity warningDisk usage above 80%
system.errorSystem error occurredUnexpected system failure

Payload Format

All webhook payloads follow this structure:

json
{
  "id": "whevt_1a2b3c4d5e6f7g8h",
  "event": "transfer.completed",
  "timestamp": "2026-04-29T15:30:00Z",
  "data": {
    // Event-specific data
  }
}

Transfer Event Payloads

transfer.completed:

json
{
  "id": "whevt_1a2b3c4d5e6f7g8h",
  "event": "transfer.completed",
  "timestamp": "2026-04-29T15:30:00Z",
  "data": {
    "transferId": "trf_1a2b3c4d5e6f",
    "agentId": "agent_1a2b3c4d",
    "agentName": "production-server-1",
    "sourceUrl": "sftp://ftp.example.com/files/data.csv",
    "destinationPath": "/local/data.csv",
    "direction": "pull",
    "protocol": "sftp",
    "size": 1048576,
    "bytesTransferred": 1048576,
    "duration": 45,
    "speed": 23303,
    "status": "completed"
  }
}

transfer.failed:

json
{
  "id": "whevt_2b3c4d5e6f7g8h9i",
  "event": "transfer.failed",
  "timestamp": "2026-04-29T15:31:00Z",
  "data": {
    "transferId": "trf_2b3c4d5e6f",
    "agentId": "agent_1a2b3c4d",
    "agentName": "production-server-1",
    "sourceUrl": "sftp://ftp.example.com/files/data.csv",
    "error": "Authentication failed: Invalid credentials",
    "errorCode": "AUTH_FAILED",
    "retryCount": 3,
    "status": "failed"
  }
}

Agent Event Payloads

agent.disconnected:

json
{
  "id": "whevt_3c4d5e6f7g8h9i0j",
  "event": "agent.disconnected",
  "timestamp": "2026-04-29T15:32:00Z",
  "data": {
    "agentId": "agent_1a2b3c4d",
    "agentName": "production-server-1",
    "hostname": "server1.example.com",
    "lastSeen": "2026-04-29T15:30:00Z",
    "status": "offline"
  }
}

agent.certificate.expiring:

json
{
  "id": "whevt_4d5e6f7g8h9i0j1k",
  "event": "agent.certificate.expiring",
  "timestamp": "2026-04-29T15:33:00Z",
  "data": {
    "agentId": "agent_1a2b3c4d",
    "agentName": "production-server-1",
    "certificateId": "cert_1a2b3c4d",
    "expiresAt": "2026-05-29T00:00:00Z",
    "daysRemaining": 30
  }
}

Job Event Payloads

job.completed:

json
{
  "id": "whevt_5e6f7g8h9i0j1k2l",
  "event": "job.completed",
  "timestamp": "2026-04-29T15:34:00Z",
  "data": {
    "jobId": "job_1a2b3c4d",
    "jobName": "Daily Backup",
    "agentId": "agent_1a2b3c4d",
    "schedule": "0 2 * * *",
    "transfersTotal": 5,
    "transfersCompleted": 5,
    "transfersFailed": 0,
    "duration": 320,
    "status": "completed"
  }
}

Security

Signature Verification

Each webhook request includes an X-Webhook-Signature header when a secret is configured. Verify this signature to ensure the request originated from MFTPlus.

The signature is an HMAC hex digest computed using the SHA-256 algorithm:

X-Webhook-Signature: sha256=<signature>

Verification Example (Node.js):

javascript
import crypto from 'crypto';

function verifySignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(payload).digest('hex');
  const expectedSignature = `sha256=${digest}`;
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Usage
const payload = request.rawBody;
const signature = request.headers['x-webhook-signature'];
const isValid = verifySignature(payload, signature, 'your-secret');

Verification Example (Python):

python
import hmac
import hashlib

def verify_signature(payload, signature, secret):
    expected_signature = 'sha256=' + hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected_signature)

# Usage
payload = request.body
signature = request.headers.get('X-Webhook-Signature')
is_valid = verify_signature(payload, signature, 'your-secret')

HTTPS Requirements

  • Always use HTTPS URLs for webhook endpoints
  • MFTPlus refuses to send webhooks to HTTP endpoints (except for localhost testing)
  • Ensure your SSL certificate is valid and not expired

IP Whitelisting

For additional security, configure your firewall to only accept webhook connections from MFTPlus IP addresses. Contact support for the current IP ranges.

Retry Policy

When your webhook endpoint returns a non-2xx status code or times out, MFTPlus retries delivery:

AttemptTiming
1st retry1 minute after failure
2nd retry5 minutes after 1st retry
3rd retry30 minutes after 2nd retry
4th retry2 hours after 3rd retry

After 4 failed attempts, the webhook is marked as failed and an alert is sent to the admin. Manual reactivation is required.

Timeout: Webhook endpoints must respond within 10 seconds. Requests timing out are retried.

Testing

Testing Webhooks Locally

Use localtunnel or ngrok to test webhooks during development:

bash
# Using localtunnel
npx localtunnel --port 3000

# Using ngrok
ngrok http 3000

Then configure your webhook with the provided HTTPS URL.

Test Event Payloads

Send a test event from the admin panel:

  1. Navigate to SettingsWebhooks
  2. Select your webhook
  3. Click Send Test Event
  4. Choose an event type to simulate

Best Practices

Endpoint Design

  • Return quickly: Process webhooks asynchronously and respond immediately
  • Return 200 OK: Always return 200-299 status, even for internal errors
  • Idempotency: Handle duplicate events (use the id field for deduplication)
  • Logging: Log all received webhooks for troubleshooting

Example Handler (Node.js/Express):

javascript
app.post('/webhooks/mftplus', async (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  // Verify signature
  if (!verifySignature(req.rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { id, event, data } = req.body;

  // Check for duplicate
  if (await Event.exists({ id })) {
    return res.status(200).send('Duplicate');
  }

  // Process asynchronously
  processEvent(event, data).catch(console.error);

  // Save event ID for deduplication
  await Event.create({ id, event, data });

  // Respond immediately
  res.status(200).send('OK');
});

Security Checklist

  • [ ] Always use HTTPS endpoints
  • [ ] Configure and verify signature secrets
  • [ ] Implement request deduplication
  • [ ] Validate payload structure
  • [ ] Log all webhook events
  • [ ] Monitor failed webhook deliveries
  • [ ] Rotate secrets periodically

Monitoring

Monitor webhook health through the admin panel:

  • Delivery Status: Success/failure rates
  • Response Times: Average endpoint response time
  • Recent Failures: Last 10 failed deliveries with error details

Set up alerts for:

  • Failed webhooks (after all retries exhausted)
  • High failure rates (>5% in last hour)
  • Slow endpoint responses (>5 seconds)

Troubleshooting

Webhooks Not Arriving

  1. Check webhook status: Verify webhook is Active in admin panel
  2. Verify URL: Ensure endpoint URL is correct and accessible
  3. Check firewall rules: Confirm MFTPlus IPs can reach your endpoint
  4. Review signature verification: Temporarily disable to test
  5. Check delivery logs: View recent delivery attempts in admin panel

Signature Verification Failing

  1. Confirm secret matches: Verify secret in webhook configuration
  2. Check raw payload: Ensure you're using the raw request body, not parsed
  3. Verify algorithm: Confirm SHA-256 HMAC calculation
  4. Test without verification: Temporarily disable to isolate the issue

High Failure Rates

  1. Check endpoint health: Verify your application is running
  2. Review response times: Ensure responses are under 10 seconds
  3. Monitor errors: Check application logs for exceptions during processing
  4. Scale infrastructure: Add capacity if endpoint is overloaded

Duplicate Events

Duplicate deliveries are normal and expected. Use the id field in each payload to deduplicate:

javascript
const processedEvents = new Set();

function handleWebhook(payload) {
  if (processedEvents.has(payload.id)) {
    return; // Skip duplicate
  }
  processedEvents.add(payload.id);
  // Process event...
}

Managing Webhooks

List Webhooks

bash
curl https://api.mftplus.co.za/api/webhooks \
  -H "Authorization: Bearer <admin-token>"

Get Webhook Details

bash
curl https://api.mftplus.co.za/api/webhooks/{id} \
  -H "Authorization: Bearer <admin-token>"

Update Webhook

bash
curl -X PATCH https://api.mftplus.co.za/api/webhooks/{id} \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <admin-token>" \
  -d '{
    "url": "https://new-url.com/webhook",
    "active": false
  }'

Delete Webhook

bash
curl -X DELETE https://api.mftplus.co.za/api/webhooks/{id} \
  -H "Authorization: Bearer <admin-token>"

View Delivery History

bash
curl https://api.mftplus.co.za/api/webhooks/{id}/deliveries \
  -H "Authorization: Bearer <admin-token>"

Example Integrations

Slack Notifications

javascript
app.post('/webhooks/mftplus', (req, res) => {
  const { event, data } = req.body;

  if (event === 'transfer.failed') {
    axios.post(process.env.SLACK_WEBHOOK_URL, {
      text: `Transfer failed: ${data.sourceUrl}`,
      attachments: [{
        color: 'danger',
        fields: [
          { title: 'Agent', value: data.agentName },
          { title: 'Error', value: data.error }
        ]
      }]
    });
  }

  res.status(200).send('OK');
});

Database Update

python
@app.route('/webhooks/mftplus', methods=['POST'])
def webhook_handler():
    payload = request.get_json()

    if payload['event'] == 'transfer.completed':
        # Update transfer record in database
        db.transfers.update_one(
            {'transfer_id': payload['data']['transferId']},
            {'$set': {
                'status': 'completed',
                'completed_at': payload['timestamp']
            }}
        )

    return '', 200

PagerDuty Alert

javascript
app.post('/webhooks/mftplus', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'agent.disconnected') {
    await axios.post(
      `https://api.pagerduty.com/incidents`,
      {
        incident: {
          type: 'incident',
          title: `MFTPlus Agent Offline: ${data.agentName}`,
          service: { id: process.env.PAGERDUTY_SERVICE_ID },
          urgency: 'high'
        }
      },
      {
        headers: {
          'Authorization': `Token token=${process.env.PAGERDUTY_TOKEN}`,
          'Accept': 'application/vnd.pagerduty+json;version=2'
        }
      }
    );
  }

  res.status(200).send('OK');
});

Reference

Request Headers

HeaderDescription
Content-TypeAlways application/json
X-Webhook-SignatureHMAC SHA-256 signature (if secret configured)
X-Webhook-IDUnique webhook delivery ID
X-Webhook-TimestampUnix timestamp of delivery attempt
User-AgentMFTPlus-Webhook/1.0

Response Requirements

  • Status Code: Return 2xx for successful processing
  • Body: Optional (ignored by MFTPlus)
  • Timeout: Respond within 10 seconds to avoid retry

Common Errors

HTTP StatusDescriptionRetry Behavior
401Invalid signatureNo retry (security issue)
404Endpoint not foundRetry (may be temporary)
408Request timeoutRetry
429Rate limitedRetry with exponential backoff
500+Server errorRetry

For more information, see API Reference or Troubleshooting.