Skip to content

Webhooks

Receive real-time notifications when jobs complete, fail, or when batches finish processing. Mend supports standard JSON webhooks with HMAC signatures and Discord-style rich embeds.

Async Delivery

Non-blocking webhook calls don’t slow down job processing

Automatic Retries

3 attempts with exponential backoff for reliability

Multiple Formats

Standard JSON or Discord embeds

Secure

HMAC-SHA256 signatures for default style

Default Style Recommended

Section titled “Default Style ”

Standard JSON payload with HMAC-SHA256 signature for verification.

Use Cases:

  • Custom webhook receivers
  • Secure integrations
  • Programmatic processing

Payload Structure:

{
"event": "job.completed",
"timestamp": "2025-10-18T22:00:00Z",
"job": {
"id": "job-abc-123",
"type": "image_resize",
"status": "completed",
"source_bucket": "input",
"source_key": "image.jpg",
"dest_bucket": "output",
"dest_key": "resized.jpg",
"result": {
"output_url": "s3://output/resized.jpg",
"file_size": 102400,
"metadata": {}
},
"created_at": "2025-10-18T21:59:00Z",
"updated_at": "2025-10-18T22:00:00Z"
}
}

Headers:

Content-Type: application/json
User-Agent: Mend-Webhook/1.0
X-Mend-Signature: abc123def456...
X-Mend-Timestamp: 2025-10-18T22:00:00Z

Rich embed format compatible with Discord webhooks for team notifications.

Use Cases:

  • Discord server notifications
  • Team alerts
  • Visual status updates

Payload Structure:

{
"username": "Mend",
"embeds": [
{
"title": "✅ Job Completed",
"description": "Job **job-abc-123** has been processed",
"color": 3066993,
"fields": [
{
"name": "Job ID",
"value": "job-abc-123",
"inline": true
},
{
"name": "Type",
"value": "image_resize",
"inline": true
},
{
"name": "Status",
"value": "completed",
"inline": true
}
],
"timestamp": "2025-10-18T22:00:00Z",
"footer": {
"text": "Mend Media Processing"
}
}
]
}

Color Codes:

  • Job Completed: Green (3066993)
  • Job Failed: Red (15158332)
  • 📦 Batch Completed: Blue (3447003)
  • 📦 Batch Failed: Red (15158332)

Set the default webhook style in config.yaml:

config.yaml
webhook:
timeout: 10s
max_retries: 3
secret: "${WEBHOOK_SECRET}" # For HMAC signatures
style: "default" # Options: "default" or "discord"
{
"source_bucket": "input",
"source_key": "image.jpg",
"dest_bucket": "output",
"dest_key": "resized.jpg",
"width": 800,
"height": 600,
"webhook_url": "https://your-app.com/webhooks/mend",
"webhook_style": "default"
}

job.completed

Job finished successfully with results

job.failed

Job encountered an error

batch.completed

All batch jobs completed

batch.failed

Batch processing failed

batch.partial

Some batch jobs succeeded, some failed

  1. Create Discord Webhook

    • Go to Discord Server Settings → Integrations → Webhooks
    • Click “New Webhook”
    • Name it (e.g., “Mend Notifications”)
    • Select the channel for notifications
    • Copy the webhook URL
  2. Use in Job Creation

    Terminal window
    curl -X POST http://localhost:8080/api/v1/jobs/image/resize \
    -H "Content-Type: application/json" \
    -H "X-API-Key: your_api_key_here" \
    -d '{
    "source_bucket": "input",
    "source_key": "photo.jpg",
    "dest_bucket": "output",
    "dest_key": "thumbnail.jpg",
    "width": 200,
    "height": 200,
    "webhooks": [
    {
    "url": "https://discord.com/api/webhooks/123456789/abcdefg...",
    "style": "discord"
    }
    ]
    }'
  3. Receive Notifications

    You’ll see rich embeds in your Discord channel with job status updates!

const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Verify HMAC signature
function verifySignature(payload, signature, timestamp) {
const data = timestamp + '.' + JSON.stringify(payload);
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(data)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks/mend', (req, res) => {
const signature = req.headers['x-mend-signature'];
const timestamp = req.headers['x-mend-timestamp'];
// Verify signature
if (!verifySignature(req.body, signature, timestamp)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
const { event, job } = req.body;
console.log(`Received ${event} for job ${job.id}`);
if (event === 'job.completed') {
// Handle successful job
console.log('Job completed:', job.result);
} else if (event === 'job.failed') {
// Handle failed job
console.error('Job failed:', job.error);
}
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook receiver listening on port 3000');
});

Signature Generation:

signature = HMAC-SHA256(secret, timestamp + "." + payload)

Verification Steps:

  1. Extract X-Mend-Signature and X-Mend-Timestamp headers
  2. Concatenate timestamp and payload: timestamp + "." + JSON.stringify(payload)
  3. Compute HMAC-SHA256 with your secret
  4. Compare computed signature with received signature using constant-time comparison
  5. Reject if signatures don’t match
const timestamp = new Date(req.headers['x-mend-timestamp']);
const now = new Date();
const fiveMinutes = 5 * 60 * 1000;
if (now - timestamp > fiveMinutes) {
return res.status(401).json({ error: 'Webhook too old' });
}

Mend automatically retries failed webhook deliveries:

Attempt 1

Immediate delivery

Attempt 2

After 2 seconds

Attempt 3

After 4 seconds

Success Criteria:

  • HTTP status code 200-299
  • Response received within timeout (default: 10s)

Failure Handling:

  • After 3 failed attempts, webhook is marked as failed
  • Error is logged but job processing continues
  • No further retry attempts
const processedJobs = new Set();
app.post('/webhooks/mend', async (req, res) => {
const { job } = req.body;
// Return 200 immediately
res.status(200).json({ received: true });
// Process asynchronously
if (processedJobs.has(job.id)) {
console.log('Duplicate webhook, skipping');
return;
}
processedJobs.add(job.id);
await processJob(job);
});

Check:

  • Webhook URL is publicly accessible
  • Firewall allows incoming connections
  • HTTPS certificate is valid
  • Endpoint returns 200 status code
  • Response time is under timeout limit

Check:

  • WEBHOOK_SECRET matches config
  • Using correct signature algorithm (HMAC-SHA256)
  • Concatenating timestamp and payload correctly
  • Using constant-time comparison

Check:

  • Using discord style
  • Webhook URL is correct
  • Embed structure is valid
  • Not exceeding Discord rate limits
{
"event": "batch.completed",
"timestamp": "2025-10-18T22:10:00Z",
"batch": {
"id": "batch-550e8400",
"status": "completed",
"total_jobs": 10,
"completed": 10,
"failed": 0,
"created_at": "2025-10-18T22:00:00Z",
"completed_at": "2025-10-18T22:10:00Z"
}
}
{
"event": "job.failed",
"timestamp": "2025-10-18T22:00:05Z",
"job": {
"id": "job-abc-123",
"type": "image_resize",
"status": "failed",
"error": "Source file not found: s3://bucket/image.jpg",
"created_at": "2025-10-18T22:00:00Z",
"updated_at": "2025-10-18T22:00:05Z"
}
}