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

# 回调通知

> 了解如何接收和验证任务完成的回调通知

# 回调通知

当您在创建视频任务时提供了 `notify_url`，Scenext会在任务完成（成功或失败）时向您的服务器发送回调通知。

## 回调数据格式

Scenext会向您的 `notify_url` 发送POST请求，包含以下数据：

<ResponseField name="task_id" type="string">
  任务ID
</ResponseField>

<ResponseField name="status" type="string">
  任务状态：`COMPLETED`、`FAILED` 或 `IN_PROGRESS`
</ResponseField>

<ResponseField name="progress" type="array">
  任务进度信息列表
</ResponseField>

<ResponseField name="result" type="array">
  任务结果（仅在完成时存在）
</ResponseField>

<ResponseField name="timestamp" type="integer">
  时间戳（Unix时间戳）
</ResponseField>

## 签名验证

为确保安全性，每个回调请求都包含HMAC-SHA256签名，位于 `X-Signature` 请求头中。

### 验证步骤

1. 获取请求体的JSON字符串
2. 使用您的API密钥生成HMAC-SHA256签名
3. 比较生成的签名与 `X-Signature` 头部的值

### Python验证示例

```python theme={null}
import hmac
import hashlib
import json
from flask import Flask, request

app = Flask(__name__)

def verify_signature(payload, signature, api_key):
    """
    验证HMAC-SHA256签名
    """
    # 将payload转换为JSON字符串（按键排序）
    if isinstance(payload, dict):
        payload_str = json.dumps(payload, sort_keys=True)
    else:
        payload_str = str(payload)
    
    # 生成签名
    expected_signature = hmac.new(
        key=api_key.encode('utf-8'),
        msg=payload_str.encode('utf-8'),
        digestmod=hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(expected_signature, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    # 获取签名
    signature = request.headers.get('X-Signature')
    if not signature:
        return 'Missing signature', 400
    
    # 获取请求数据
    data = request.get_json()
    
    # 验证签名
    api_key = "YOUR_API_KEY"  # 您的API密钥
    if not verify_signature(data, signature, api_key):
        return 'Invalid signature', 401
    
    # 处理回调数据
    task_id = data.get('task_id')
    status = data.get('status')
    result = data.get('result')
    
    if status == 'COMPLETED':
        # 任务完成，处理结果
        video_url = result.get('video_url')
        print(f"Task {task_id} completed. Video URL: {video_url}")
    elif status == 'FAILED':
        # 任务失败，处理错误
        error = result.get('error')
        print(f"Task {task_id} failed. Error: {error}")
    
    return 'OK', 200
```

### Node.js验证示例

```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();

app.use(express.json());

function verifySignature(payload, signature, apiKey) {
    // 将payload转换为JSON字符串（按键排序）
    const payloadStr = JSON.stringify(payload, Object.keys(payload).sort());
    
    // 生成签名
    const expectedSignature = crypto
        .createHmac('sha256', apiKey)
        .update(payloadStr, 'utf8')
        .digest('hex');
    
    return crypto.timingSafeEqual(
        Buffer.from(expectedSignature, 'hex'),
        Buffer.from(signature, 'hex')
    );
}

app.post('/webhook', (req, res) => {
    const signature = req.headers['x-signature'];
    if (!signature) {
        return res.status(400).send('Missing signature');
    }
    
    const data = req.body;
    const apiKey = 'YOUR_API_KEY';  // 您的API密钥
    
    if (!verifySignature(data, signature, apiKey)) {
        return res.status(401).send('Invalid signature');
    }
    
    // 处理回调数据
    const { task_id, status, result } = data;
    
    if (status === 'COMPLETED') {
        console.log(`Task ${task_id} completed. Video URL: ${result.video_url}`);
    } else if (status === 'FAILED') {
        console.log(`Task ${task_id} failed. Error: ${result.error}`);
    }
    
    res.status(200).send('OK');
});
```

## 回调示例

### 任务完成回调

```json theme={null}
{
    "task_id": "12345",
    "status": "COMPLETED",
    "progress": [
        {
            "id": "frame1",
            "image": "https://oss.scenext.cn/frame_img/d988d4d9-72f0-4b76-97b0-ca974335dcf4/ed6e7bae-cb5d-4e75-a6ae-b4beced80c92/frame1.png?OSSAccessKeyId=LTAI5tDkzckmQzw7eKj4s47h&Expires=1749287113&Signature=ZofSGtAyhO3KolVaMC8ICUZJNWE%3D"
        },
        {
            "id": "frame2",
            "image": "https://oss.scenext.cn/frame_img/d988d4d9-72f0-4b76-97b0-ca974335dcf4/ed6e7bae-cb5d-4e75-a6ae-b4beced80c92/frame3.png?OSSAccessKeyId=LTAI5tDkzckmQzw7eKj4s47h&Expires=1749287134&Signature=5zB4nuv7PQkPDCzbKLMQBlb9f%2Bo%3D"
        },
        ...
    ],
    "result": [
        {
            "type": "video",
            "content": "https://oss.scenext.cn/Video/d988d4d9-72f0-4b76-97b0-ca974335dcf4/Manimed6e7bae-cb5d-4e75-a6ae-b4beced80c92.mp4?OSSAccessKeyId=LTAI5tDkzckmQzw7eKj4s47h&Expires=1749288010&Signature=CZRkEsLpWctM9MS3v7IbpRvMcKI%3D"
        }
    ],
    "timestamp": 1672531200
}
```

### 任务失败回调

```json theme={null}
{
    "task_id": "12345",
    "status": "FAILED",
    "progress": [
        {
            "id": "frame1",
            "image": "https://oss.scenext.cn/frame_img/d988d4d9-72f0-4b76-97b0-ca974335dcf4/ed6e7bae-cb5d-4e75-a6ae-b4beced80c92/frame1.png?OSSAccessKeyId=LTAI5tDkzckmQzw7eKj4s47h&Expires=1749287113&Signature=ZofSGtAyhO3KolVaMC8ICUZJNWE%3D"
        },
        {
            "id": "frame2",
            "image": "https://oss.scenext.cn/frame_img/d988d4d9-72f0-4b76-97b0-ca974335dcf4/ed6e7bae-cb5d-4e75-a6ae-b4beced80c92/frame3.png?OSSAccessKeyId=LTAI5tDkzckmQzw7eKj4s47h&Expires=1749287134&Signature=5zB4nuv7PQkPDCzbKLMQBlb9f%2Bo%3D"
        },
        ...
    ],
    "result": [
        {
            "type": "text",
            "content": "任务执行失败"
        }
    ],
    "timestamp": 1672531200
}
```

## 提示

<Warning>
  * 请务必验证回调签名以确保安全性
  * 回调通知无需返回响应，该回调通知只会发送一次
</Warning>
