> ## Documentation Index
> Fetch the complete documentation index at: https://docs.videobgremover.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks - Real-Time Job Notifications

> Get instant notifications when video processing completes. Learn how to use webhooks with cURL, API client, and SDKs instead of polling.

## 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)

```typescript theme={"dark"}
// ❌ 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)

```typescript theme={"dark"}
// ✅ 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

<Info>
  The system waits for your complete video (with composition/export) before sending the `job.completed` webhook. This prevents premature notifications.
</Info>

***

## 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:

```bash theme={"dark"}
# 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:**

```json theme={"dark"}
{
  "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:

<CodeGroup>
  ```typescript Node.js theme={"dark"}
  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')
  ```

  ```python Python theme={"dark"}
  from videobgremover import VideoBGRemoverClient
  import os

  client = VideoBGRemoverClient(api_key=os.getenv('API_KEY'))

  # Create job from file
  job = client.create_job_file({
      'file': './video.mp4'
  })

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

  print('Job started, webhook will notify when complete')
  ```
</CodeGroup>

***

## Using Webhooks via SDK (High-Level)

Use the `Video` class for simpler workflows:

<CodeGroup>
  ```typescript Node.js theme={"dark"}
  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')
  ```

  ```python Python theme={"dark"}
  from videobgremover import VideoBGRemoverClient, Video, RemoveBGOptions, Prefer
  import os

  client = VideoBGRemoverClient(api_key=os.getenv('API_KEY'))

  # Load video
  video = Video.open('./video.mp4')

  # Remove background with webhook
  foreground = video.remove_background(
      client,
      RemoveBGOptions(prefer=Prefer.WEBM_VP9),
      webhook_url='https://your-app.com/webhooks'
  )

  print('Processing started, webhook will notify when complete')
  ```
</CodeGroup>

<Note>
  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.
</Note>

***

## Webhook Events & Payloads

Your webhook endpoint will receive POST requests with these payloads:

### job.started

Fires when processing begins.

```json theme={"dark"}
{
  "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.

```json theme={"dark"}
{
  "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"
    }
  }
}
```

<Info>
  To download the processed video, call the status endpoint: `GET /v1/jobs/{id}/status` which returns the `processed_video_url` field with the download URL.
</Info>

### job.failed

Fires when processing or export fails.

```json theme={"dark"}
{
  "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:

```http theme={"dark"}
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

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

### SDK Methods

<CodeGroup>
  ```typescript Node.js theme={"dark"}
  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}`)
    }
  })
  ```

  ```python Python theme={"dark"}
  deliveries = client.webhook_deliveries(job_id)

  print(f"Total deliveries: {deliveries['total_deliveries']}")

  for d in deliveries['deliveries']:
      print(f"{d['event_type']}: {d['delivery_status']} (attempt {d['attempt_number']})")
      if d['error_message']:
          print(f"  Error: {d['error_message']}")
  ```
</CodeGroup>

**Response:**

```json theme={"dark"}
{
  "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:

```typescript theme={"dark"}
// ✅ 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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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

<Warning>
  **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.
</Warning>

***

## Next Steps

* [API Reference](/api-reference/introduction) - Complete API documentation
* [Examples](/examples) - Webhook server code examples
* [SDK Reference](/sdk-reference/client) - Full SDK documentation
