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

# Webhooks

> Receive real-time notifications when tasks complete

## Overview

Instead of polling for task status, configure a webhook to receive notifications when events occur. ModelHunter.AI will POST a JSON payload to your URL.

## Register a Webhook

```bash theme={null}
curl -X POST https://api.modelhunter.ai/api/v1/webhooks \
  -H "Authorization: Bearer river_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/modelhunter",
    "events": ["task.completed", "task.failed"]
  }'
```

### Event Types

| Event            | Description                             |
| ---------------- | --------------------------------------- |
| `task.completed` | A generation task finished successfully |
| `task.failed`    | A generation task failed                |

## Webhook Payload

### Headers

```http theme={null}
Content-Type: application/json
X-Webhook-ID: evt_abc123
X-Webhook-Timestamp: 1705312800
X-Webhook-Signature: sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

### Body

<CodeGroup>
  ```json task.completed theme={null}
  {
    "id": "evt_abc123",
    "type": "task.completed",
    "created_at": "2025-01-15T10:00:45Z",
    "data": {
      "task": {
        "id": "task_abc123",
        "type": "video",
        "status": "succeeded",
        "provider": "vidu",
        "model": "viduq3-turbo",
        "result": [
          {
            "url": "https://cdn.modelhunter.ai/results/task_abc123.mp4?signature=xxx",
            "duration": 4,
            "format": "mp4",
            "size_bytes": 12582912
          }
        ],
        "created_at": "2025-01-15T10:00:00Z",
        "completed_at": "2025-01-15T10:00:45Z",
        "metadata": { "user_id": "u_123" }
      }
    }
  }
  ```

  ```json task.failed theme={null}
  {
    "id": "evt_def456",
    "type": "task.failed",
    "created_at": "2025-01-15T10:00:10Z",
    "data": {
      "task": {
        "id": "task_def456",
        "type": "video",
        "status": "failed",
        "provider": "vidu",
        "model": "viduq3-turbo",
        "error": {
          "code": "PROVIDER_ERROR",
          "message": "Content policy violation"
        },
        "created_at": "2025-01-15T10:00:00Z",
        "completed_at": "2025-01-15T10:00:10Z"
      }
    }
  }
  ```
</CodeGroup>

## Per-Job Webhooks vs Configured Webhooks

There are two types of webhook delivery:

* **Configured webhooks** (registered via `POST /api/v1/webhooks`) include `X-Webhook-Signature` for verification and support automatic retries.
* **Per-job webhooks** (via `webhookUrl` in a generation request) do **not** include `X-Webhook-Signature` (no shared secret), but include additional headers: `X-Webhook-Event` (e.g. `task.completed`) and `X-Job-ID`.

## Signature Verification

Configured webhooks include an `X-Webhook-Signature` header. Verify it to ensure the request is authentic.

The signature is computed as `HMAC-SHA256(timestamp + "." + body, secret)`.

<CodeGroup>
  ```javascript JavaScript theme={null}
  import crypto from "crypto";

  function verifyWebhookSignature(req, secret) {
    const timestamp = req.headers["x-webhook-timestamp"];
    const signature = req.headers["x-webhook-signature"];
    const body = JSON.stringify(req.body);

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${body}`)
      .digest("hex");

    const expectedSignature = `sha256=${expected}`;

    if (signature !== expectedSignature) {
      throw new Error("Invalid webhook signature");
    }

    // Reject timestamps older than 5 minutes
    const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
    if (age > 300) {
      throw new Error("Webhook timestamp too old");
    }

    return true;
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import json

  def verify_webhook_signature(headers, body, secret):
      timestamp = headers["X-Webhook-Timestamp"]
      signature = headers["X-Webhook-Signature"]

      payload = f"{timestamp}.{json.dumps(body)}"
      expected = hmac.new(
          secret.encode(),
          payload.encode(),
          hashlib.sha256,
      ).hexdigest()

      expected_signature = f"sha256={expected}"

      if not hmac.compare_digest(signature, expected_signature):
          raise ValueError("Invalid webhook signature")

      # Reject timestamps older than 5 minutes
      age = int(time.time()) - int(timestamp)
      if age > 300:
          raise ValueError("Webhook timestamp too old")

      return True
  ```
</CodeGroup>

## Retry Policy

If your endpoint does not return a `2xx` status, ModelHunter.AI retries with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | 1 minute   |
| 2       | 5 minutes  |
| 3       | 30 minutes |

After 3 failed attempts, the delivery is marked as failed. You can replay it from the Dashboard.

## Test a Webhook

Send a test event to verify your endpoint:

```bash theme={null}
curl -X POST https://api.modelhunter.ai/api/v1/webhooks/{webhook_id}/test \
  -H "Authorization: Bearer river_live_xxx"
```

## Manage Webhooks

```bash theme={null}
# List webhooks
curl https://api.modelhunter.ai/api/v1/webhooks \
  -H "Authorization: Bearer river_live_xxx"

# Delete a webhook
curl -X DELETE https://api.modelhunter.ai/api/v1/webhooks/{webhook_id} \
  -H "Authorization: Bearer river_live_xxx"
```
