Skip to main content

Why Use Webhooks?

Instead of repeatedly calling the API to check if your video is done (polling), webhooks notify you instantly when processing completes.

Polling (Inefficient)

// ❌ Wastes resources, adds latency
while (true) {
  const status = await client.status(jobId)
  if (status.status === 'completed') break
  await sleep(5000) // Check every 5 seconds
}

Webhooks (Efficient)

// ✅ Instant notification, no wasted API calls
await client.startJob(jobId, {
  webhook_url: 'https://your-app.com/webhooks/videobgremover',
  background: { type: 'transparent' }
})
// Server notifies you when done
Benefits:
  • Real-time: Get notified instantly when processing completes
  • Efficient: No repeated API calls or bandwidth waste
  • Scalable: Process multiple videos without constant polling
When to use webhooks vs polling:
  • Use webhooks for production apps, automation, background processing
  • ⚠️ Use polling for quick scripts, testing, or if you can’t receive HTTP requests

How VideoBGRemover Webhooks Work

When you start a job with a webhook_url, our system:
  1. Validates your webhook URL (HTTPS required in production)
  2. Stores the URL with your job
  3. Triggers 3 events during processing:
    • job.started - Processing begins
    • job.completed - Video ready (includes output URLs)
    • job.failed - Processing failed (includes error message)
  4. Sends POST request to your URL with JSON payload
  5. Retries up to 3 times (10 seconds apart) if delivery fails
  6. Waits for BOTH background removal AND export to complete before final webhook
The system waits for your complete video (with composition/export) before sending the job.completed webhook. This prevents premature notifications.

Quick Setup

Your webhook endpoint must:
  1. Accept POST requests with Content-Type: application/json
  2. Return a 2xx status code quickly (< 5 seconds)
  3. Use HTTPS (production only - HTTP allowed for testing)

Using Webhooks via cURL

Add webhook_url to your job start request:
# 1. Create job
JOB=$(curl -s -X POST https://api.videobgremover.com/v1/jobs \
  -H "X-Api-Key: $API_KEY" \
  -F "file=@video.mp4")

JOB_ID=$(echo $JOB | jq -r '.id')

# 2. Start job with webhook
curl -X POST https://api.videobgremover.com/v1/jobs/$JOB_ID/start \
  -H "X-Api-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-app.com/webhooks",
    "background": {
      "type": "transparent",
      "transparent_format": "webm_vp9"
    }
  }'
Response:
{
  "message": "Job started",
  "status": "processing",
  "webhook_url": "https://your-app.com/webhooks"
}

Using Webhooks via API Client (Low-Level)

Use the VideoBGRemoverClient class for direct API control:
import { VideoBGRemoverClient } from 'videobgremover'

const client = new VideoBGRemoverClient(process.env.API_KEY!)

// Create job from file
const job = await client.createJobFile({
  file: './video.mp4'
})

// Start with webhook
await client.startJob(job.id, {
  webhook_url: 'https://your-app.com/webhooks',
  background: {
    type: 'transparent',
    transparent_format: 'webm_vp9'
  }
})

console.log('Job started, webhook will notify when complete')

Using Webhooks via SDK (High-Level)

Use the Video class for simpler workflows:
import { VideoBGRemoverClient, Video, RemoveBGOptions, Prefer } from 'videobgremover'

const client = new VideoBGRemoverClient(process.env.API_KEY!)

// Load video
const video = await Video.open('./video.mp4')

// Remove background with webhook
const foreground = await video.removeBackground({
  client,
  webhookUrl: 'https://your-app.com/webhooks',
  options: new RemoveBGOptions(Prefer.WEBM_VP9)
})

console.log('Processing started, webhook will notify when complete')
Webhook support for the high-level video.removeBackground() SDK method is coming soon. For now, use the low-level client.startJob() API shown above for webhook functionality.

Webhook Events & Payloads

Your webhook endpoint will receive POST requests with these payloads:

job.started

Fires when processing begins.
{
  "event": "job.started",
  "timestamp": "2025-11-01T12:30:00.123Z",
  "job": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "processing",
    "filename": "my-video.mp4",
    "created_at": "2025-11-01T12:30:00.000Z"
  }
}

job.completed

Fires when BOTH background removal AND export complete.
{
  "event": "job.completed",
  "timestamp": "2025-11-01T12:35:00.456Z",
  "job": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "export_id": "exp_webm_vp9_123",
    "status": "completed",
    "filename": "my-video.mp4",
    "length_seconds": 30.5,
    "output_format": "webm",
    "background": {
      "type": "transparent",
      "transparent_format": "webm_vp9"
    }
  }
}
To download the processed video, call the job result endpoint: GET /v1/jobs/{id}/result which returns the download URL.

job.failed

Fires when processing or export fails.
{
  "event": "job.failed",
  "timestamp": "2025-11-01T12:32:00.789Z",
  "job": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "failed",
    "filename": "my-video.mp4",
    "message": "Video file too large: 1250.5MB exceeds limit of 1000MB"
  }
}

HTTP Headers

Webhooks include these headers:
POST /your/webhook/endpoint
Content-Type: application/json
User-Agent: VideoBGRemover-Webhooks/1.0
X-VideoBGRemover-Event: job.completed
X-VideoBGRemover-Job-ID: 550e8400-e29b-41d4-a716-446655440000
X-VideoBGRemover-Timestamp: 2025-11-01T12:35:00.456Z
X-VideoBGRemover-Attempt: 1

Checking Delivery History

View webhook delivery attempts and results:

API Endpoint

curl "https://api.videobgremover.com/v1/webhooks/deliveries?video_id=$JOB_ID" \
  -H "X-Api-Key: $API_KEY"

SDK Methods

const deliveries = await client.webhookDeliveries(jobId)

console.log(`Total deliveries: ${deliveries.total_deliveries}`)

deliveries.deliveries.forEach(d => {
  console.log(`${d.event_type}: ${d.delivery_status} (attempt ${d.attempt_number})`)
  if (d.error_message) {
    console.log(`  Error: ${d.error_message}`)
  }
})
Response:
{
  "video_id": "550e8400-e29b-41d4-a716-446655440000",
  "total_deliveries": 2,
  "deliveries": [
    {
      "event_type": "job.started",
      "webhook_url": "https://your-app.com/webhooks",
      "attempt_number": 1,
      "delivery_status": "delivered",
      "http_status_code": 200,
      "error_message": null,
      "scheduled_at": "2025-11-01T12:30:00.000Z",
      "delivered_at": "2025-11-01T12:30:00.234Z"
    },
    {
      "event_type": "job.completed",
      "webhook_url": "https://your-app.com/webhooks",
      "attempt_number": 1,
      "delivery_status": "delivered",
      "http_status_code": 200,
      "error_message": null,
      "scheduled_at": "2025-11-01T12:35:00.000Z",
      "delivered_at": "2025-11-01T12:35:00.567Z"
    }
  ]
}

Best Practices

Return 200 Immediately

Process webhooks asynchronously to avoid timeouts:
// ✅ Good: Return immediately, process async
app.post('/webhooks', async (req, res) => {
  res.status(200).send('OK') // Return first

  // Process async
  await processWebhook(req.body).catch(console.error)
})

// ❌ Bad: Slow processing blocks response
app.post('/webhooks', async (req, res) => {
  await downloadVideo(req.body.job.processed_video_url) // Slow!
  res.status(200).send('OK') // Timeout risk
})

Handle Retries

Use attempt_number to detect retries:
app.post('/webhooks', (req, res) => {
  const { job } = req.body
  const attempt = parseInt(req.headers['x-videobgremover-attempt'])

  if (attempt > 1) {
    console.log(`Retry attempt ${attempt} for job ${job.id}`)
  }

  // Process idempotently
  res.status(200).send('OK')
})

Validate Events

Check event type before processing:
const VALID_EVENTS = ['job.started', 'job.completed', 'job.failed']

if (!VALID_EVENTS.includes(req.body.event)) {
  return res.status(400).send('Invalid event')
}

Use HTTPS

Production webhooks require HTTPS for security.

Common Issues

Webhook Not Received

  1. Check delivery history using the API or SDK
  2. Verify HTTPS (required in production)
  3. Check firewall - ensure your server is publicly accessible
  4. Test with webhook.site to verify our system is sending correctly

Duplicate Webhooks

This is expected during retries. Implement idempotency:
const processedJobs = new Set()

app.post('/webhooks', (req, res) => {
  const jobId = req.body.job.id

  if (processedJobs.has(jobId)) {
    return res.status(200).send('Already processed')
  }

  processedJobs.add(jobId)
  // Process webhook...
})

Timeout Errors

  • Optimize your webhook endpoint to respond in < 5 seconds
  • Return 200 immediately, process asynchronously
  • Check delivery history to see timeout errors
Security Note: Webhooks do not currently include HMAC signatures. Use unpredictable webhook URLs (include random tokens) and validate job IDs via the API if security is critical.

Next Steps