Documentation

Veedeo API V3 Webhooks Guide

Comprehensive Webhook Implementation Guide Last Updated: January 17, 2025

Table of Contents

  1. Overview
  2. Webhook Configuration
  3. Event Types
  4. Security Implementation
  5. Event Filtering
  6. Implementation Examples
  7. Error Handling
  8. Testing and Debugging
  9. Best Practices
  10. Troubleshooting

Overview

Webhooks provide real-time notifications about your video rendering tasks. Instead of polling the API repeatedly, your application receives HTTP POST requests when events occur, enabling efficient real-time updates and automated workflows.

Key Benefits

  • Real-time Updates: Immediate notifications when task status changes
  • Efficient: No need for continuous polling
  • Reliable: Built-in retry mechanism with exponential backoff
  • Secure: HMAC-SHA256 signature verification
  • Flexible: Configurable event filtering

Webhook Flow

1. Create render task with webhook configuration
2. Veedeo processes the task
3. Events trigger HTTP POST to your endpoint
4. Your server processes the event
5. Return 2xx response to acknowledge receipt

Webhook Configuration

⚠️ Important: Webhook URL is Required

The webhook.url field is mandatory for all render requests. Without it, you will not receive notifications when your render completes, and you won't get the download URL for your rendered video. The webhook is the only way to receive render results.

Basic Configuration

{
  "webhook": {
    "url": "https://your-app.com/webhooks/veedeo",
    "events": ["task.completed", "task.failed"]
  }
}
{
  "webhook": {
    "url": "https://your-app.com/webhooks/veedeo",
    "events": ["task.completed", "task.failed"],
    "secret": "your_webhook_secret_key"
  }
}

Advanced Configuration

{
  "webhook": {
    "url": "https://your-app.com/webhooks/veedeo",
    "events": ["task.started", "task.progress", "task.completed", "task.failed"],
    "secret": "your_webhook_secret_key",
    "headers": {
      "X-Custom-Header": "your-value",
      "X-Environment": "production"
    },
    "timeout_seconds": 30,
    "max_retries": 3
  }
}

Configuration Options

FieldTypeRequiredDescription
urlstringYesYour webhook endpoint URL (HTTPS required) - Essential for receiving render results
eventsarrayNoEvent types to receive (default: all)
secretstringNoSecret key for HMAC signature verification (recommended for security)
headersobjectNoCustom headers to include in webhook requests
timeout_secondsnumberNoRequest timeout (default: 30, max: 60)
max_retriesnumberNoMaximum retry attempts (default: 3, max: 5)

Event Types

task.queued

Sent when a task is successfully queued for processing.

{
  "event": "task.queued",
  "timestamp": "2025-01-17T10:00:00Z",
  "task_id": "tsk_1234567890abcdef",
  "data": {
    "queue_position": 5,
    "estimated_start_time": "2025-01-17T10:02:00Z",
    "priority": "standard"
  }
}

Use Cases:

  • Update UI to show task is queued
  • Log task creation for analytics
  • Set initial progress indicators

task.started

Sent when rendering begins.

{
  "event": "task.started",
  "timestamp": "2025-01-17T10:00:15Z",
  "task_id": "tsk_1234567890abcdef",
  "data": {
    "started_at": "2025-01-17T10:00:15Z",
    "estimated_completion": "2025-01-17T10:02:15Z",
    "worker_id": "worker_abc123"
  }
}

Use Cases:

  • Show "Processing" status in UI
  • Start progress tracking
  • Update estimated completion time

task.progress

Sent periodically during rendering (maximum once every 30 seconds).

{
  "event": "task.progress",
  "timestamp": "2025-01-17T10:01:00Z",
  "task_id": "tsk_1234567890abcdef",
  "data": {
    "progress": {
      "percentage": 45,
      "current_phase": "render",
      "phase_progress": 0.75,
      "estimated_completion": "2025-01-17T10:02:15Z"
    },
    "phases": {
      "download": {
        "completed": true,
        "duration_seconds": 5
      },
      "render": {
        "completed": false,
        "progress": 0.75
      },
      "upload": {
        "completed": false,
        "progress": 0.0
      }
    }
  }
}

Use Cases:

  • Update progress bars in real-time
  • Show current processing phase
  • Provide detailed progress feedback

task.completed

Sent when rendering completes successfully.

{
  "event": "task.completed",
  "timestamp": "2025-01-17T10:02:30Z",
  "task_id": "tsk_1234567890abcdef",
  "data": {
    "completed_at": "2025-01-17T10:02:30Z",
    "processing_time_seconds": 135,
    "output": {
      "video_url": "https://storage.veedeo.dev/renders/tsk_1234.mp4",
      "thumbnail_url": "https://storage.veedeo.dev/thumbnails/tsk_1234.jpg",
      "preview_url": "https://storage.veedeo.dev/previews/tsk_1234.gif",
      "duration_ms": 15000,
      "file_size_bytes": 12582912,
      "resolution": {
        "width": 1920,
        "height": 1080
      },
      "format": "mp4",
      "expires_at": "2025-01-18T10:02:30Z"
    },
    "performance": {
      "estimated_vs_actual": {
        "estimated_seconds": 120,
        "actual_seconds": 135,
        "accuracy": 0.89
      }
    }
  }
}

Use Cases:

  • Download and store output files
  • Show completion notification
  • Update UI with video player
  • Analytics and performance tracking

task.failed

Sent when rendering fails.

{
  "event": "task.failed",
  "timestamp": "2025-01-17T10:01:30Z",
  "task_id": "tsk_1234567890abcdef",
  "data": {
    "failed_at": "2025-01-17T10:01:30Z",
    "processing_time_seconds": 75,
    "error": {
      "code": "MEDIA_DOWNLOAD_FAILED",
      "message": "Unable to download media file from provided URL",
      "details": {
        "media_url": "https://example.com/unreachable.jpg",
        "http_status": 404,
        "retry_after_seconds": 300
      },
      "retry_possible": true,
      "suggested_action": "Verify media URL accessibility and retry"
    }
  }
}

Use Cases:

  • Show error messages to users
  • Implement automatic retry logic
  • Log errors for debugging
  • Send failure notifications

task.canceled

Sent when a task is manually canceled.

{
  "event": "task.canceled",
  "timestamp": "2025-01-17T10:01:15Z",
  "task_id": "tsk_1234567890abcdef",
  "data": {
    "canceled_at": "2025-01-17T10:01:15Z",
    "canceled_by": "user_request",
    "processing_time_seconds": 45
  }
}

Use Cases:

  • Update UI to show cancellation
  • Clean up related resources
  • Log cancellation events

Security Implementation

HMAC-SHA256 Signature Verification

All webhooks include a signature header for verification:

X-Veedeo-Signature: sha256=abcdef1234567890...
X-Veedeo-Timestamp: 1642434600

Implementation Examples

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  const receivedSignature = signature.replace('sha256=', '');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(receivedSignature, 'hex')
  );
}

// Express middleware
function verifyWebhook(secret) {
  return (req, res, next) => {
    const signature = req.headers['x-veedeo-signature'];
    const timestamp = req.headers['x-veedeo-timestamp'];

    if (!signature || !timestamp) {
      return res.status(401).json({ error: 'Missing signature headers' });
    }

    // Check timestamp to prevent replay attacks (5 minutes tolerance)
    const now = Math.floor(1735776000); // Example timestamp
    if (Math.abs(now - parseInt(timestamp)) > 300) {
      return res.status(401).json({ error: 'Request too old' });
    }

    if (!verifyWebhookSignature(req.body, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    next();
  };
}

Python

import hmac
import hashlib
import json
import time

def verify_webhook_signature(payload: dict, signature: str, secret: str) -> bool:
    """Verify HMAC-SHA256 signature for webhook security."""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        json.dumps(payload, separators=(',', ':')).encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    received_signature = signature.replace('sha256=', '')
    return hmac.compare_digest(expected_signature, received_signature)

def verify_timestamp(timestamp: str, tolerance_seconds: int = 300) -> bool:
    """Verify timestamp to prevent replay attacks."""
    try:
        webhook_time = int(timestamp)
        current_time = int(time.time())
        return abs(current_time - webhook_time) <= tolerance_seconds
    except (ValueError, TypeError):
        return False

# Flask decorator
from functools import wraps
from flask import request, jsonify

def verify_webhook(secret: str):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            signature = request.headers.get('X-Veedeo-Signature', '')
            timestamp = request.headers.get('X-Veedeo-Timestamp', '')

            if not signature or not timestamp:
                return jsonify({'error': 'Missing signature headers'}), 401

            if not verify_timestamp(timestamp):
                return jsonify({'error': 'Request too old'}), 401

            payload = request.get_json()
            if not verify_webhook_signature(payload, signature, secret):
                return jsonify({'error': 'Invalid signature'}), 401

            return f(*args, **kwargs)
        return decorated_function
    return decorator

PHP

function verifyWebhookSignature($payload, $signature, $secret) {
    $expectedSignature = hash_hmac('sha256', json_encode($payload), $secret);
    $receivedSignature = str_replace('sha256=', '', $signature);

    return hash_equals($expectedSignature, $receivedSignature);
}

function verifyTimestamp($timestamp, $toleranceSeconds = 300) {
    $webhookTime = (int)$timestamp;
    $currentTime = time();

    return abs($currentTime - $webhookTime) <= $toleranceSeconds;
}

// Usage in webhook handler
$signature = $_SERVER['HTTP_X_VEEDEO_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_VEEDEO_TIMESTAMP'] ?? '';

if (!$signature || !$timestamp) {
    http_response_code(401);
    echo json_encode(['error' => 'Missing signature headers']);
    exit;
}

if (!verifyTimestamp($timestamp)) {
    http_response_code(401);
    echo json_encode(['error' => 'Request too old']);
    exit;
}

$payload = json_decode(file_get_contents('php://input'), true);

if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

Event Filtering

Configure which events you want to receive to reduce noise and processing overhead.

Basic Filtering

{
  "webhook": {
    "url": "https://your-app.com/webhook",
    "events": ["task.completed", "task.failed"]
  }
}

Use Case-Specific Filtering

Minimal Notifications (Production)

{
  "webhook": {
    "url": "https://your-app.com/webhook/production",
    "events": ["task.completed", "task.failed"]
  }
}

Development/Testing

{
  "webhook": {
    "url": "https://your-app.com/webhook/dev",
    "events": ["task.started", "task.progress", "task.completed", "task.failed"]
  }
}

Analytics Only

{
  "webhook": {
    "url": "https://analytics.your-app.com/veedeo",
    "events": ["task.completed"],
    "headers": {
      "X-Analytics-Source": "veedeo-api"
    }
  }
}

Implementation Examples

Complete Express.js Handler

const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// Webhook verification middleware
app.use('/webhook/veedeo', verifyWebhook(process.env.WEBHOOK_SECRET));

// Main webhook handler
app.post('/webhook/veedeo', async (req, res) => {
  const { event, task_id, data } = req.body;

  try {
    await handleWebhookEvent(event, task_id, data);
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function handleWebhookEvent(event, taskId, data) {
  switch (event) {
    case 'task.queued':
      await handleTaskQueued(taskId, data);
      break;

    case 'task.started':
      await handleTaskStarted(taskId, data);
      break;

    case 'task.progress':
      await handleTaskProgress(taskId, data);
      break;

    case 'task.completed':
      await handleTaskCompleted(taskId, data);
      break;

    case 'task.failed':
      await handleTaskFailed(taskId, data);
      break;

    case 'task.canceled':
      await handleTaskCanceled(taskId, data);
      break;

    default:
      console.log(`Unknown event type: ${event}`);
  }
}

async function handleTaskCompleted(taskId, data) {
  const { output } = data;

  // 1. Update database
  await updateTaskStatus(taskId, 'completed', {
    videoUrl: output.video_url,
    thumbnailUrl: output.thumbnail_url,
    expiresAt: output.expires_at
  });

  // 2. Download files before expiry
  await downloadAndStoreFiles(taskId, output);

  // 3. Send user notification
  await sendUserNotification(taskId, 'completed');

  // 4. Trigger downstream processes
  await triggerPostProcessing(taskId, output);
}

async function downloadAndStoreFiles(taskId, output) {
  const downloads = [
    downloadFile(output.video_url, `renders/${taskId}.mp4`),
    downloadFile(output.thumbnail_url, `thumbnails/${taskId}.jpg`),
    downloadFile(output.preview_url, `previews/${taskId}.gif`)
  ];

  await Promise.all(downloads);
}

async function handleTaskFailed(taskId, data) {
  const { error } = data;

  // Log error for debugging
  console.error(`Task ${taskId} failed:`, error);

  // Update database
  await updateTaskStatus(taskId, 'failed', { error });

  // Implement retry logic for retryable errors
  if (error.retry_possible && await shouldRetry(taskId)) {
    await scheduleRetry(taskId, error.retry_after_seconds || 300);
  } else {
    // Send failure notification
    await sendUserNotification(taskId, 'failed', error.message);
  }
}

async function shouldRetry(taskId) {
  const retryCount = await getRetryCount(taskId);
  return retryCount < 3; // Max 3 retries
}

FastAPI Handler (Python)

from fastapi import FastAPI, HTTPException, Depends, Request
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import asyncio

app = FastAPI()

class WebhookEvent(BaseModel):
    event: str
    timestamp: str
    task_id: str
    data: Dict[str, Any]

@app.post("/webhook/veedeo")
async def handle_webhook(
    webhook_event: WebhookEvent,
    request: Request,
    _: None = Depends(verify_webhook_signature)
):
    try:
        await process_webhook_event(webhook_event)
        return {"received": True}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

async def process_webhook_event(webhook_event: WebhookEvent):
    handlers = {
        'task.queued': handle_task_queued,
        'task.started': handle_task_started,
        'task.progress': handle_task_progress,
        'task.completed': handle_task_completed,
        'task.failed': handle_task_failed,
        'task.canceled': handle_task_canceled,
    }

    handler = handlers.get(webhook_event.event)
    if handler:
        await handler(webhook_event.task_id, webhook_event.data)
    else:
        print(f"Unknown event type: {webhook_event.event}")

async def handle_task_completed(task_id: str, data: Dict[str, Any]):
    output = data['output']

    # Run tasks concurrently
    await asyncio.gather(
        update_task_status(task_id, 'completed', output),
        download_and_store_files(task_id, output),
        send_user_notification(task_id, 'completed'),
        trigger_post_processing(task_id, output)
    )

async def download_and_store_files(task_id: str, output: Dict[str, Any]):
    downloads = [
        download_file(output['video_url'], f'renders/{task_id}.mp4'),
        download_file(output['thumbnail_url'], f'thumbnails/{task_id}.jpg'),
        download_file(output['preview_url'], f'previews/{task_id}.gif')
    ]

    await asyncio.gather(*downloads)

Laravel Handler (PHP)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Services\WebhookProcessor;
use App\Http\Middleware\VerifyVeedeoWebhook;

class VeedeoWebhookController extends Controller
{
    public function __construct()
    {
        $this->middleware(VerifyVeedeoWebhook::class);
    }

    public function handle(Request $request, WebhookProcessor $processor)
    {
        $event = $request->input('event');
        $taskId = $request->input('task_id');
        $data = $request->input('data', []);

        try {
            $processor->process($event, $taskId, $data);
            return response()->json(['received' => true]);
        } catch (\Exception $e) {
            \Log::error('Webhook processing failed', [
                'event' => $event,
                'task_id' => $taskId,
                'error' => $e->getMessage()
            ]);

            return response()->json(['error' => 'Processing failed'], 500);
        }
    }
}

// WebhookProcessor Service
class WebhookProcessor
{
    public function process(string $event, string $taskId, array $data): void
    {
        switch ($event) {
            case 'task.completed':
                $this->handleTaskCompleted($taskId, $data);
                break;

            case 'task.failed':
                $this->handleTaskFailed($taskId, $data);
                break;

            case 'task.progress':
                $this->handleTaskProgress($taskId, $data);
                break;

            default:
                \Log::info("Unknown webhook event: {$event}");
        }
    }

    private function handleTaskCompleted(string $taskId, array $data): void
    {
        $output = $data['output'];

        // Update database
        Task::where('id', $taskId)->update([
            'status' => 'completed',
            'video_url' => $output['video_url'],
            'thumbnail_url' => $output['thumbnail_url'],
            'expires_at' => $output['expires_at']
        ]);

        // Queue file download job
        DownloadRenderFiles::dispatch($taskId, $output);

        // Send notification
        $user = Task::find($taskId)->user;
        $user->notify(new RenderCompleted($taskId, $output));
    }
}

Error Handling

Webhook Delivery Failures

Veedeo implements automatic retry with exponential backoff:

  1. Initial Attempt: Immediate delivery
  2. Retry 1: After 30 seconds
  3. Retry 2: After 2 minutes
  4. Retry 3: After 8 minutes
  5. Final Retry: After 32 minutes

Response Requirements

Your webhook endpoint must:

  • Respond with HTTP status 2xx (200-299) within 30 seconds
  • Return valid JSON (optional but recommended)
  • Handle duplicate events idempotently

Example Error Responses

Temporary Error (Retry)

app.post('/webhook', (req, res) => {
  try {
    // Process webhook
    res.status(200).json({ received: true });
  } catch (error) {
    if (error.isTemporary) {
      // Return 5xx for temporary errors (triggers retry)
      res.status(503).json({ error: 'Temporary service unavailable' });
    } else {
      // Return 4xx for permanent errors (no retry)
      res.status(400).json({ error: 'Invalid webhook data' });
    }
  }
});

Idempotency Handling

const processedEvents = new Set();

app.post('/webhook', (req, res) => {
  const { event, task_id, timestamp } = req.body;
  const eventKey = `${event}_${task_id}_${timestamp}`;

  if (processedEvents.has(eventKey)) {
    console.log('Duplicate event received, ignoring');
    return res.status(200).json({ received: true, duplicate: true });
  }

  try {
    processEvent(req.body);
    processedEvents.add(eventKey);
    res.status(200).json({ received: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Testing and Debugging

Local Testing with ngrok

# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# In another terminal, expose it
ngrok http 3000

# Use the HTTPS URL in webhook configuration
# https://abc123.ngrok.io/webhook/veedeo

Webhook Testing Tool

// test-webhook.js
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook/test', (req, res) => {
  console.log('=== Webhook Received ===');
  console.log('Headers:', req.headers);
  console.log('Body:', JSON.stringify(req.body, null, 2));
  console.log('========================');

  res.status(200).json({ received: true });
});

app.listen(3000, () => {
  console.log('Test webhook server running on port 3000');
});

Webhook Validator

function validateWebhookPayload(payload) {
  const requiredFields = ['event', 'timestamp', 'task_id', 'data'];

  for (const field of requiredFields) {
    if (!payload.hasOwnProperty(field)) {
      throw new Error(`Missing required field: ${field}`);
    }
  }

  const validEvents = [
    'task.queued', 'task.started', 'task.progress',
    'task.completed', 'task.failed', 'task.canceled'
  ];

  if (!validEvents.includes(payload.event)) {
    throw new Error(`Invalid event type: ${payload.event}`);
  }

  return true;
}

Debug Logging

app.post('/webhook/veedeo', (req, res) => {
  const startTime = 1735776000000;

  console.log(`[${new Date().toISOString()}] Webhook received:`, {
    event: req.body.event,
    task_id: req.body.task_id,
    headers: {
      signature: req.headers['x-veedeo-signature'],
      timestamp: req.headers['x-veedeo-timestamp']
    }
  });

  try {
    processWebhook(req.body);

    const duration = 1735776005000 - startTime;
    console.log(`[${new Date().toISOString()}] Webhook processed in ${duration}ms`);

    res.status(200).json({ received: true, processing_time_ms: duration });
  } catch (error) {
    console.error(`[${new Date().toISOString()}] Webhook error:`, error);
    res.status(500).json({ error: error.message });
  }
});

Best Practices

1. Implement Proper Security

  • Always verify HMAC signatures
  • Check timestamp to prevent replay attacks
  • Use HTTPS for webhook URLs
  • Rotate webhook secrets regularly

2. Handle Events Idempotently

// Store processed events to prevent duplicates
const redis = require('redis');
const client = redis.createClient();

async function processWebhookIdempotently(event, taskId, timestamp) {
  const eventKey = `webhook:${event}:${taskId}:${timestamp}`;

  const exists = await client.exists(eventKey);
  if (exists) {
    console.log('Event already processed, skipping');
    return;
  }

  await processEvent(event, taskId);

  // Mark as processed (expire after 24 hours)
  await client.setex(eventKey, 86400, '1');
}

3. Implement Graceful Error Handling

async function processWebhook(webhookData) {
  try {
    await processEvent(webhookData);
  } catch (error) {
    if (error.isRetryable) {
      // Log and let Veedeo retry
      console.error('Retryable error:', error);
      throw error;
    } else {
      // Log and acknowledge to prevent retries
      console.error('Non-retryable error:', error);
      await logError(webhookData, error);
      // Don't throw - return success to stop retries
    }
  }
}

4. Use Async Processing for Heavy Operations

const Queue = require('bull');
const fileProcessingQueue = new Queue('file processing');

app.post('/webhook/veedeo', async (req, res) => {
  const { event, task_id, data } = req.body;

  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Queue heavy processing
  if (event === 'task.completed') {
    await fileProcessingQueue.add('download-files', {
      taskId: task_id,
      output: data.output
    });
  }
});

// Process files in background
fileProcessingQueue.process('download-files', async (job) => {
  const { taskId, output } = job.data;
  await downloadAndStoreFiles(taskId, output);
});

5. Monitor Webhook Health

// Track webhook processing metrics
const webhookMetrics = {
  received: 0,
  processed: 0,
  failed: 0,
  avgProcessingTime: 0
};

app.post('/webhook/veedeo', async (req, res) => {
  const startTime = 1735776000000;
  webhookMetrics.received++;

  try {
    await processWebhook(req.body);
    webhookMetrics.processed++;

    const duration = 1735776005000 - startTime;
    webhookMetrics.avgProcessingTime =
      (webhookMetrics.avgProcessingTime + duration) / 2;

    res.status(200).json({ received: true });
  } catch (error) {
    webhookMetrics.failed++;
    res.status(500).json({ error: error.message });
  }
});

// Health check endpoint
app.get('/webhook/health', (req, res) => {
  res.json({
    status: 'healthy',
    metrics: webhookMetrics,
    uptime: process.uptime()
  });
});

Troubleshooting

Common Issues

1. Signature Verification Failures

Problem: Invalid signature errors

Solutions:

  • Ensure secret matches webhook configuration
  • Use exact JSON string (no formatting changes)
  • Check character encoding (UTF-8)
  • Verify HMAC implementation
// Debug signature generation
function debugSignature(payload, secret) {
  const jsonString = JSON.stringify(payload);
  console.log('JSON string:', jsonString);
  console.log('JSON bytes:', Buffer.from(jsonString).toString('hex'));

  const signature = crypto
    .createHmac('sha256', secret)
    .update(jsonString)
    .digest('hex');

  console.log('Generated signature:', signature);
  return signature;
}

2. Timeout Issues

Problem: Webhook processing takes too long

Solutions:

  • Acknowledge immediately, process asynchronously
  • Use background job queues
  • Optimize database queries
  • Implement caching

3. Duplicate Events

Problem: Same event received multiple times

Solutions:

  • Implement idempotency checking
  • Use event timestamp + task_id as unique key
  • Store processed events in cache/database

4. Missing Events

Problem: Expected webhooks not received

Solutions:

  • Check webhook URL accessibility
  • Verify webhook configuration
  • Check server logs for errors
  • Test with webhook.site for debugging

Debug Checklist

  • Webhook URL is accessible via HTTPS
  • Endpoint returns 2xx status codes
  • Signature verification is implemented correctly
  • Response time is under 30 seconds
  • Idempotency handling is in place
  • Error logging is configured
  • Monitoring is set up

Testing Webhook Configuration

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

# Expected: 2xx response

For additional support, contact webhook-support@veedeo.dev