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

# Webhooks

> Get notified the moment your videos finish rendering

Instead of polling, configure a webhook endpoint once and TrueFan will `POST` to it whenever a request you submitted finishes — one delivery per request, regardless of how many rows it contained.

## 1. Create an endpoint

```bash theme={null}
curl -X POST "https://dev-backend-ai.truefans.in/api/external/v1/webhooks/" \
  -H "Authorization: Bearer $TRUEFAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_url": "https://myapp.example.com/webhooks/truefan"}'
```

```json Response theme={null}
{
  "webhook": {
    "endpoint_id": "9d2f1e0a-...",
    "target_url": "https://myapp.example.com/webhooks/truefan",
    "is_active": true,
    "created": "2026-08-21T10:00:00Z"
  },
  "secret": "b1946ac92492d2347c6235b4d2611184..."
}
```

<Warning>
  `secret` is returned **exactly once**, in the create response — the same reveal-once posture as the API key itself. Store it now; you'll need it to verify incoming deliveries.
</Warning>

## 2. Verify incoming requests

Every delivery is signed with HMAC-SHA256 over the raw request body, using the endpoint's secret:

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac

  def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
      # signature_header looks like "sha256=<hex digest>"
      expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature_header)
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifySignature(rawBody, signatureHeader, secret) {
    const expected =
      "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
  }
  ```
</CodeGroup>

<Note>
  Compute the HMAC over the **raw, unparsed** request body — not a re-serialized version of the parsed JSON, which can differ in key order or whitespace and produce a mismatched signature.
</Note>

## Request format

<ResponseField name="X-TrueFan-Signature" type="header">
  `sha256=<hex-encoded HMAC-SHA256 digest>` of the raw body, using your endpoint's secret.
</ResponseField>

<ResponseField name="X-TrueFan-Event" type="header">
  The event type — same value as the `event` field in the body.
</ResponseField>

### `bulk_job.completed`

Fired once every row in a request has reached a terminal state — whether you sent one row or a thousand, this fires exactly once per request, never once per row.

```json theme={null}
{
  "event": "bulk_job.completed",
  "bulk_job_id": "8c1f2e3a-...",
  "total": 50,
  "success": 48,
  "failed": 2,
  "csv_export_path": "/api/external/v1/bulk-jobs/8c1f2e3a-.../export.csv"
}
```

This payload is intentionally just the summary counts, not the full result list. Download the complete batch — every row's `result_video_url`, `external_id`, and `generation_id` — from [Download CSV export](/docs/api-reference/status/export-bulk-job-csv); `csv_export_path` above is that exact path, relative to your API host, ready to fetch as soon as this webhook arrives.

## Delivery & retries

Your endpoint should return a `2xx` status within 10 seconds. Anything else is treated as a failure:

* Retried with exponential backoff — 30s, 60s, 120s, ... up to a 1-hour cap — for up to 6 attempts.
* After the final attempt fails, the delivery is marked `dead` and not retried again.

Check delivery history — including response codes and final status — via [List webhook deliveries](/docs/api-reference/webhooks/list-webhook-deliveries):

```bash theme={null}
curl "https://dev-backend-ai.truefans.in/api/external/v1/webhooks/{endpoint_id}/deliveries/" \
  -H "Authorization: Bearer $TRUEFAN_API_KEY"
```

<Tip>
  If a delivery shows as `dead`, your endpoint was unreachable or erroring for the entire retry window — check your own logs for that time range, then fall back to [polling](/docs/guides/polling-for-status) for that specific job to recover the result.
</Tip>

## Removing an endpoint

```bash theme={null}
curl -X DELETE "https://dev-backend-ai.truefans.in/api/external/v1/webhooks/{endpoint_id}/" \
  -H "Authorization: Bearer $TRUEFAN_API_KEY"
```
