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

# Webhooks Overview

> Get real-time notifications about your video generation and export jobs

## What are Webhooks?

Webhooks allow you to receive real-time notifications when your video generation or export jobs complete. Instead of polling the status endpoint repeatedly, Hoox will send an HTTP POST request to your specified URL when the job finishes.

## Benefits

<CardGroup cols={2}>
  <Card title="Real-time Updates" icon="bolt">
    Get notified immediately when jobs complete, no need to poll
  </Card>

  <Card title="Reduced API Calls" icon="chart-line-down">
    Save on rate limits by eliminating status polling
  </Card>

  <Card title="Better UX" icon="heart">
    Provide instant feedback to your users
  </Card>

  <Card title="Automation" icon="robot">
    Trigger downstream processes automatically
  </Card>
</CardGroup>

## How Webhooks Work

<Steps>
  <Step title="Configure Webhook URL">
    Include a `webhook_url` in your generation or export request
  </Step>

  <Step title="Job Processing">
    Hoox processes your video generation or export job
  </Step>

  <Step title="Webhook Delivery">
    When the job completes (success or failure), Hoox sends a POST request to your URL
  </Step>

  <Step title="Acknowledgment">
    Your endpoint should respond with a 200 status code to confirm receipt
  </Step>
</Steps>

## Webhook Payload

All webhooks include a `task` field to identify the type of operation. This helps you route and process webhooks correctly.

### Payload Structure by Task Type

| Field       | video\_generate | video\_export  | avatar\_create | avatar\_edit   |
| ----------- | --------------- | -------------- | -------------- | -------------- |
| `task`      | ✅               | ✅              | ✅              | ✅              |
| `job_id`    | ✅               | ✅              | ❌              | ❌              |
| `avatar_id` | ❌               | ❌              | ✅              | ✅              |
| `look_id`   | ❌               | ❌              | ✅              | ✅              |
| `status`    | ✅               | ✅              | ✅              | ✅              |
| `result`    | ✅               | ✅              | ✅              | ✅              |
| `error`     | ✅ (on failure)  | ✅ (on failure) | ✅ (on failure) | ✅ (on failure) |

<Info>
  **Key differences:**

  * Video operations use `job_id` (the Trigger.dev run ID)
  * Avatar operations use `avatar_id` and `look_id` instead
  * Always check the `task` field to determine how to process the webhook
</Info>

<Tabs>
  <Tab title="Video Generation">
    ### Successful Generation

    ```json theme={null}
    {
      "task": "video_generate",
      "job_id": "run_01234567890abcdef",
      "status": "completed",
      "result": {
        "video_id": "video_abcdef1234567890",
        "thumbnail_url": "https://cdn.hoox.video/thumbnails/video_123.jpg",
        "cost": 5,
        "created_at": "2024-01-15T10:30:00Z"
      }
    }
    ```

    ### Failed Generation

    ```json theme={null}
    {
      "task": "video_generate",
      "job_id": "run_01234567890abcdef",
      "status": "failed",
      "error": {
        "code": "INSUFFICIENT_CREDITS",
        "message": "Not enough credits to complete video generation"
      }
    }
    ```
  </Tab>

  <Tab title="Video Export">
    ### Successful Export

    ```json theme={null}
    {
      "task": "video_export",
      "job_id": "run_fedcba0987654321",
      "status": "completed", 
      "result": {
        "video_id": "video_abcdef1234567890",
        "download_url": "https://cdn.hoox.video/exports/video_456.mp4",
        "cost": 3,
        "created_at": "2024-01-15T10:35:00Z"
      }
    }
    ```

    ### Failed Export

    ```json theme={null}
    {
      "task": "video_export",
      "job_id": "run_fedcba0987654321",
      "status": "failed",
      "error": {
        "code": "EXPORT_FAILED",
        "message": "Render failed"
      }
    }
    ```
  </Tab>

  <Tab title="Avatar Creation">
    ### Successful Creation

    ```json theme={null}
    {
      "task": "avatar_create",
      "avatar_id": "avatar_abc123",
      "look_id": "look_def456",
      "status": "completed",
      "result": {
        "thumbnail_url": "https://cdn.hoox.video/avatars/look_def456.jpg"
      }
    }
    ```

    ### Failed Creation

    ```json theme={null}
    {
      "task": "avatar_create",
      "avatar_id": "avatar_abc123",
      "look_id": "look_def456",
      "status": "failed",
      "error": {
        "code": "GENERATION_FAILED",
        "message": "Unknown error during generation"
      }
    }
    ```
  </Tab>

  <Tab title="Avatar Edit">
    ### Successful Edit

    ```json theme={null}
    {
      "task": "avatar_edit",
      "avatar_id": "avatar_abc123",
      "look_id": "look_ghi789",
      "status": "completed",
      "result": {
        "thumbnail_url": "https://cdn.hoox.video/avatars/look_ghi789.jpg"
      }
    }
    ```

    ### Failed Edit

    ```json theme={null}
    {
      "task": "avatar_edit",
      "avatar_id": "avatar_abc123",
      "look_id": "look_ghi789",
      "status": "failed",
      "error": {
        "code": "GENERATION_FAILED",
        "message": "Edit failed"
      }
    }
    ```
  </Tab>
</Tabs>

## Setting Up Webhooks

### 1. Create an Endpoint

Your webhook endpoint should:

* Accept POST requests
* Respond with 200 status code
* Process the payload asynchronously if needed

<CodeGroup>
  ```javascript Node.js/Express theme={null}
  app.post('/webhook/hoox', express.json(), (req, res) => {
    const { task, job_id, avatar_id, look_id, status, result, error } = req.body;
    
    console.log(`Webhook received for ${task}: ${status}`);
    
    // Route based on task type
    switch (task) {
      case 'video_generate':
        handleVideoGeneration(job_id, status, result, error);
        break;
      case 'video_export':
        handleVideoExport(job_id, status, result, error);
        break;
      case 'avatar_create':
      case 'avatar_edit':
        handleAvatar(avatar_id, look_id, status, result, error);
        break;
      default:
        console.warn('Unknown task type:', task);
    }
    
    // Always respond with 200
    res.status(200).send('OK');
  });

  function handleVideoGeneration(job_id, status, result, error) {
    if (status === 'completed') {
      console.log('Video generated:', result.video_id);
      updateJobStatus(job_id, 'completed', result);
      notifyUser(job_id, result);
    } else {
      console.error('Generation failed:', error);
      updateJobStatus(job_id, 'failed', null, error);
      notifyUserOfFailure(job_id, error);
    }
  }

  function handleVideoExport(job_id, status, result, error) {
    if (status === 'completed') {
      console.log('Video exported:', result.download_url);
      updateJobStatus(job_id, 'completed', result);
      notifyUser(job_id, result);
    } else {
      console.error('Export failed:', error);
      updateJobStatus(job_id, 'failed', null, error);
      notifyUserOfFailure(job_id, error);
    }
  }

  function handleAvatar(avatar_id, look_id, status, result, error) {
    if (status === 'completed') {
      console.log('Avatar ready:', avatar_id, look_id);
      updateAvatarStatus(avatar_id, look_id, 'completed', result);
      notifyUser(avatar_id, result);
    } else {
      console.error('Avatar failed:', error);
      updateAvatarStatus(avatar_id, look_id, 'failed', null, error);
      notifyUserOfFailure(avatar_id, error);
    }
  }
  ```

  ```python Python/Flask theme={null}
  from flask import Flask, request, jsonify

  @app.route('/webhook/hoox', methods=['POST'])
  def hoox_webhook():
      data = request.get_json()
      
      task = data.get('task')
      job_id = data.get('job_id')
      avatar_id = data.get('avatar_id')
      look_id = data.get('look_id')
      status = data.get('status')
      result = data.get('result')
      error = data.get('error')
      
      print(f"Webhook received for {task}: {status}")
      
      # Route based on task type
      if task == 'video_generate':
          handle_video_generation(job_id, status, result, error)
      elif task == 'video_export':
          handle_video_export(job_id, status, result, error)
      elif task in ['avatar_create', 'avatar_edit']:
          handle_avatar(avatar_id, look_id, status, result, error)
      else:
          print(f"Unknown task type: {task}")
      
      # Always respond with 200
      return jsonify({'status': 'ok'}), 200

  def handle_video_generation(job_id, status, result, error):
      if status == 'completed':
          print(f"Video generated: {result.get('video_id')}")
          update_job_status(job_id, 'completed', result)
          notify_user(job_id, result)
      else:
          print(f"Generation failed: {error}")
          update_job_status(job_id, 'failed', None, error)
          notify_user_of_failure(job_id, error)

  def handle_video_export(job_id, status, result, error):
      if status == 'completed':
          print(f"Video exported: {result.get('download_url')}")
          update_job_status(job_id, 'completed', result)
          notify_user(job_id, result)
      else:
          print(f"Export failed: {error}")
          update_job_status(job_id, 'failed', None, error)
          notify_user_of_failure(job_id, error)

  def handle_avatar(avatar_id, look_id, status, result, error):
      if status == 'completed':
          print(f"Avatar ready: {avatar_id}/{look_id}")
          update_avatar_status(avatar_id, look_id, 'completed', result)
          notify_user(avatar_id, result)
      else:
          print(f"Avatar failed: {error}")
          update_avatar_status(avatar_id, look_id, 'failed', None, error)
          notify_user_of_failure(avatar_id, error)
  ```

  ```php PHP theme={null}
  <?php
  // webhook.php

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

  $task = $payload['task'] ?? 'unknown';
  $jobId = $payload['job_id'] ?? null;
  $avatarId = $payload['avatar_id'] ?? null;
  $lookId = $payload['look_id'] ?? null;
  $status = $payload['status'];
  $result = $payload['result'] ?? null;
  $error = $payload['error'] ?? null;

  error_log("Webhook received for $task: $status");

  // Route based on task type
  switch ($task) {
      case 'video_generate':
          handleVideoGeneration($jobId, $status, $result, $error);
          break;
      case 'video_export':
          handleVideoExport($jobId, $status, $result, $error);
          break;
      case 'avatar_create':
      case 'avatar_edit':
          handleAvatar($avatarId, $lookId, $status, $result, $error);
          break;
      default:
          error_log("Unknown task type: $task");
  }

  function handleVideoGeneration($jobId, $status, $result, $error) {
      if ($status === 'completed') {
          $videoId = $result['video_id'] ?? '';
          error_log("Video generated: $videoId");
          updateJobStatus($jobId, 'completed', $result);
          notifyUser($jobId, $result);
      } else {
          error_log("Generation failed: " . $error['message']);
          updateJobStatus($jobId, 'failed', null, $error);
          notifyUserOfFailure($jobId, $error);
      }
  }

  function handleVideoExport($jobId, $status, $result, $error) {
      if ($status === 'completed') {
          $downloadUrl = $result['download_url'] ?? '';
          error_log("Video exported: $downloadUrl");
          updateJobStatus($jobId, 'completed', $result);
          notifyUser($jobId, $result);
      } else {
          error_log("Export failed: " . $error['message']);
          updateJobStatus($jobId, 'failed', null, $error);
          notifyUserOfFailure($jobId, $error);
      }
  }

  function handleAvatar($avatarId, $lookId, $status, $result, $error) {
      if ($status === 'completed') {
          error_log("Avatar ready: $avatarId/$lookId");
          updateAvatarStatus($avatarId, $lookId, 'completed', $result);
          notifyUser($avatarId, $result);
      } else {
          error_log("Avatar failed: " . $error['message']);
          updateAvatarStatus($avatarId, $lookId, 'failed', null, $error);
          notifyUserOfFailure($avatarId, $error);
      }
  }

  // Always respond with 200
  http_response_code(200);
  echo 'OK';
  ?>
  ```
</CodeGroup>

### 2. Use in API Calls

Include your webhook URL when starting jobs:

<CodeGroup>
  ```javascript Generation theme={null}
  const response = await fetch('https://app.hoox.video/api/public/v1/generation/start', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer hx_live_your_api_key_here',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      prompt: "Create a video about space exploration",
      voice_id: "en-US-JennyNeural",
      webhook_url: "https://your-app.com/webhook/hoox"
    })
  });

  // Will receive webhook with task: "video_generate"
  ```

  ```javascript Export theme={null}
  const response = await fetch('https://app.hoox.video/api/public/v1/export/start', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer hx_live_your_api_key_here',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      video_id: "video_123",
      format: "vertical",
      webhook_url: "https://your-app.com/webhook/hoox"
    })
  });

  // Will receive webhook with task: "video_export"
  ```

  ```javascript Avatar Create theme={null}
  const response = await fetch('https://app.hoox.video/api/public/v1/avatar/create', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer hx_live_your_api_key_here',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      prompt: "Professional business avatar in modern office",
      style: "studio",
      webhook_url: "https://your-app.com/webhook/hoox"
    })
  });

  // Will receive webhook with task: "avatar_create"
  ```

  ```javascript Avatar Edit theme={null}
  const response = await fetch('https://app.hoox.video/api/public/v1/avatar/edit', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer hx_live_your_api_key_here',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      avatar_id: "avatar_abc123",
      look_id: "look_def456",
      prompt: "Casual outfit in coffee shop",
      style: "podcast",
      webhook_url: "https://your-app.com/webhook/hoox"
    })
  });

  // Will receive webhook with task: "avatar_edit"
  ```
</CodeGroup>

## Security Considerations

### 1. Validate Requests

Verify that webhook requests are coming from Hoox:

```javascript theme={null}
// Check User-Agent header
const userAgent = req.headers['user-agent'];
if (!userAgent || !userAgent.includes('Hoox-API')) {
  return res.status(401).send('Unauthorized');
}

// Check for Hoox webhook header
const isHooxWebhook = req.headers['x-hoox-webhook'];
if (isHooxWebhook !== 'true') {
  return res.status(401).send('Unauthorized');
}
```

### 2. Use HTTPS

Always use HTTPS URLs for your webhook endpoints to ensure data is encrypted in transit.

### 3. Implement Idempotency

Handle duplicate webhook deliveries gracefully:

```javascript theme={null}
const processedJobs = new Set();

app.post('/webhook/hoox', (req, res) => {
  const { task, job_id, avatar_id, look_id } = req.body;
  
  // Create unique ID based on task type
  const uniqueId = task === 'avatar_create' || task === 'avatar_edit' 
    ? `${avatar_id}:${look_id}` 
    : job_id;
  
  // Check if we've already processed this webhook
  if (processedJobs.has(uniqueId)) {
    console.log(`Duplicate webhook for ${uniqueId}, ignoring`);
    return res.status(200).send('OK');
  }
  
  // Process the webhook
  processWebhook(req.body);
  
  // Mark as processed
  processedJobs.add(uniqueId);
  
  res.status(200).send('OK');
});
```

## Retry Behavior

Hoox will retry webhook deliveries if your endpoint:

* Returns a non-2xx status code
* Times out (after 10 seconds)
* Is unreachable

**Retry Schedule:**

* 1st retry: After 1 second
* 2nd retry: After 2 seconds
* 3rd retry: After 4 seconds
* **Maximum**: 3 retry attempts

<Warning>
  If all retries fail, the webhook will be discarded. Ensure your endpoint is reliable and responds quickly.
</Warning>

## Testing Webhooks

### 1. Use ngrok for Local Development

```bash theme={null}
# Install ngrok
npm install -g ngrok

# Expose your local server
ngrok http 3000

# Use the HTTPS URL in your webhook_url
# https://abc123.ngrok.io/webhook/hoox
```

### 2. Webhook Testing Tools

* **RequestBin**: Create temporary endpoints to inspect payloads
* **Webhook.site**: Test and debug webhook deliveries
* **Postman**: Mock webhook requests for testing

### 3. Test Endpoint

Create a simple test endpoint to verify webhook delivery:

```javascript theme={null}
app.post('/webhook/test', (req, res) => {
  console.log('Webhook Headers:', req.headers);
  console.log('Webhook Body:', JSON.stringify(req.body, null, 2));
  console.log('Task Type:', req.body.task);
  
  // Log different IDs based on task type
  if (req.body.task === 'avatar_create' || req.body.task === 'avatar_edit') {
    console.log('Avatar ID:', req.body.avatar_id);
    console.log('Look ID:', req.body.look_id);
  } else {
    console.log('Job ID:', req.body.job_id);
  }
  
  res.status(200).send('Webhook received successfully');
});
```

## Best Practices

<AccordionGroup>
  <Accordion title="Task-Based Routing">
    * Always check the `task` field first to route webhooks correctly
    * Use different handlers for different task types
    * Validate that required fields exist based on task type
    * For avatars: use `avatar_id` and `look_id`
    * For videos: use `job_id`
  </Accordion>

  <Accordion title="Response Time">
    * Respond with 200 status as quickly as possible
    * Process heavy operations asynchronously
    * Use queues for complex workflows
  </Accordion>

  <Accordion title="Error Handling">
    * Log all webhook deliveries for debugging
    * Handle malformed payloads gracefully
    * Implement monitoring and alerting
    * Check for required fields based on task type
  </Accordion>

  <Accordion title="Scalability">
    * Use load balancers for high-volume webhooks
    * Implement rate limiting on your endpoint
    * Consider using message queues for processing
  </Accordion>
</AccordionGroup>

## Common Issues

| Issue                 | Cause                       | Solution                                                      |
| --------------------- | --------------------------- | ------------------------------------------------------------- |
| Webhooks not received | Endpoint unreachable        | Check URL and firewall settings                               |
| Duplicate processing  | No idempotency check        | Implement unique ID tracking (job\_id or avatar\_id:look\_id) |
| Slow processing       | Heavy operations in handler | Move to background jobs                                       |
| Missing webhooks      | Endpoint returning errors   | Fix endpoint logic and status codes                           |
| Wrong task routing    | Not checking task field     | Always check the `task` field first                           |
| Missing IDs           | Accessing wrong fields      | Use `job_id` for videos, `avatar_id`/`look_id` for avatars    |

<Tip>
  Always test your webhook endpoints thoroughly before using them in production.
</Tip>
