# Webhooks

Async tasks such as video generation can register a callback via `callback_url`. When a task reaches a terminal state (`succeeded` / `failed`), XRToken will POST to your URL — no polling needed.

## Enable callbacks

Add `callback_url` to your `POST /v1/videos/generations` request body:

```json
{
  "model": "doubao-seedance-2-0-260128",
  "content": [{ "type": "text", "text": "..." }],
  "callback_url": "https://your.app/webhooks/xrtoken"
}
```

## Payload format

```json
{
  "task_id": "5e1f2c8a-...",
  "status": "succeeded",
  "model": "doubao-seedance-2-0-260128",
  "video_url": "https://tos.../video.mp4",
  "created_at": "2026-04-15T10:00:00Z",
  "finished_at": "2026-04-15T10:01:32Z"
}
```

On failure:

```json
{
  "task_id": "5e1f2c8a-...",
  "status": "failed",
  "model": "doubao-seedance-2-0-260128",
  "error": { "code": "task_failed", "message": "..." },
  "created_at": "...",
  "finished_at": "..."
}
```

## Request headers

| Header | Description |
|---|---|
| `Content-Type` | `application/json` |
| `X-XRToken-Timestamp` | Send timestamp (Unix seconds) |
| `X-XRToken-Signature` | `sha256=<hex>`, HMAC-SHA256 signature |

## Verify the signature

Signature computation:

```
HMAC_SHA256(secret, timestamp + "." + raw_body)
```

`secret` is your account's Webhook Secret (Dashboard > Settings > Webhooks; starts with `whsec_`).

Node.js example:

```js
import crypto from 'node:crypto'

function verify(req, secret) {
  const ts = req.headers['x-xrtoken-timestamp']
  const sig = req.headers['x-xrtoken-signature']  // 'sha256=...'
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(ts + '.' + req.rawBody)  // Note: use the raw unparsed body
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
}
```

Python example:

```python
import hmac, hashlib

def verify(headers, raw_body, secret):
    ts = headers['x-xrtoken-timestamp']
    sig = headers['x-xrtoken-signature'].split('=', 1)[1]
    expected = hmac.new(
        secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig, expected)
```

After verifying the signature, check that `timestamp` is within 5 minutes of current time to prevent replay attacks.

## Response and retries

- Your endpoint must return `2xx` within **10 seconds**; otherwise the attempt is considered failed
- Failed/timed-out deliveries are retried up to **3 times** at intervals of `5s / 30s / 5min`
- After 4 total failures the delivery is marked `dropped` and will not be retried; task results remain available via `GET /v1/videos/generations/{taskId}`

## Idempotency

The same `task_id` callback may arrive more than once during retries. Your endpoint should use `task_id` as an idempotency key.
