新网站 App 接口接入文档

# 新网站 App 接口接入文档

更新日期:2026-09-15

本文提供给新网站配套 App 的 iOS、Android 和联调开发人员。App 统一请求 App API 网关,不直接请求网站后端、生成微服务、数据库或对象存储。新站上线前,应由服务端提供实际的 `APP_API_BASE_URL`、`SITE_ID` 和 `APP_ID`。

## 1. 接入参数

| 名称 | 示例 | 说明 |
|------|------|------|
| `APP_API_BASE_URL` | `https://app-api.example.com` | App 唯一业务 API 域名,不要使用网页域名代替 |
| `SITE_ID` | UUID | 新网站的站点 ID,区分用户、套餐、余额、内容和创作记录 |
| `APP_ID` | `com.example.app` | App 标识,通常与 iOS Bundle ID 一致 |
| `APP_VERSION` | `1.0.0` | 当前客户端版本 |
| `LOCALE` | `zh-CN` | 当前语言地区 |

本文示例中的占位符不能原样用于生产环境。

## 2. 公共请求约定

### 2.1 公共 Header

| Header | 必填条件 | 格式与用途 |
|--------|----------|------------|
| `Content-Type` | JSON POST 必填 | `application/json` |
| `Accept` | 建议 | `application/json` |
| `X-Site-Id` | 所有站点业务接口必填 | 新网站分配的 `SITE_ID` |
| `Authorization` | 登录后的接口必填 | `Bearer <accessToken>` |
| `X-Locale` | 建议 | 如 `zh-CN`、`en-US` |
| `X-App-Version` | 建议 | 如 `1.0.0` |
| `X-Device-Context` | 分流和更新检查必填 | base64url 编码的设备上下文 JSON |
| `X-App-Id` | 多 App 登录码场景必填 | 目标 App 标识 |
| `X-IOS-Proof` | 后端开启严格校验时必填 | 可信验证服务签发的一次性请求证明 |

网关在缺少 `X-Site-Id` 时可能补默认站点,但 App 不应依赖此行为。新站 App 必须显式发送自己的 `SITE_ID`,防止数据落到其他站点。

### 2.2 统一响应

网站代理接口通常返回:

```json
{
  "code": 0,
  "info": "ok",
  "data": {}
}
```

客户端必须同时判断 HTTP 状态码和 `code`。只有 HTTP 2xx 且 `code === 0` 才按业务成功处理;不要根据 `info` 文案编写固定分支。

网关自有接口可能直接返回业务对象,或者返回:

```json
{
  "error": "invalid_device_context"
}
```

通用错误:

| HTTP | 含义 | App 处理建议 |
|------|------|--------------|
| `400` | 参数或设备上下文错误 | 检查请求,不自动重试 |
| `401` | 未登录、Token 过期或登录码失效 | 清理登录态或重新获取登录码 |
| `403` | 站点/App 不匹配或 App 证明失败 | 停止重试并上报配置问题 |
| `404` | 路径、资源或 App 发版记录不存在 | 检查环境与版本配置 |
| `405` | HTTP 方法错误 | 修正请求方法 |
| `429` | 请求过于频繁 | 按退避策略稍后重试 |
| `502` | 网关无法访问上游 | 提示暂时不可用,可有限重试 |
| `503` | 上游功能或依赖未就绪 | 提示暂时不可用,不视为业务成功 |

所有敏感接口响应均按不可缓存处理。不得把密码、访问令牌、登录码、IAP 凭据和 `X-IOS-Proof` 写入日志、埋点或崩溃报告。

## 3. 设备上下文

`X-Device-Context` 是下面 JSON 的 base64url 编码结果,不带 `=` padding 也应可被服务端解析:

```json
{
  "v": 1,
  "ts": 1789430400000,
  "ap": {
    "v": "1.0.0",
    "b": "1",
    "p": "ios",
    "id": "com.example.app"
  },
  "dev": {
    "em": 0,
    "jb": 0,
    "vpn": null
  },
  "adj": {
    "net": "Organic",
    "camp": "",
    "grp": "",
    "cre": ""
  }
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `v` | integer | 是 | 上下文协议版本,当前为 `1` |
| `ts` | integer | 是 | 毫秒时间戳 |
| `ap.v` | string | 是 | App 语义版本号 |
| `ap.b` | string | 是 | 构建号 |
| `ap.p` | string | 是 | `ios` 或 `android` |
| `ap.id` | string | 是 | Bundle ID 或 Android 包名 |
| `dev.em` | `0\|1` | 是 | 是否模拟器 |
| `dev.jb` | `0\|1` | 是 | 是否越狱或 Root |
| `dev.vpn` | `0\|1\|null` | 否 | VPN 状态 |
| `adj.net` | string | 否 | Adjust network |
| `adj.camp` | string | 否 | Adjust campaign |
| `adj.grp` | string | 否 | Adjust ad group |
| `adj.cre` | string | 否 | Adjust creative |

## 4. 接口总览

| 方法 | App 请求路径 | 登录 | 用途 |
|------|--------------|------|------|
| `GET` | `/api/create-page/config` | 否 | 获取创作页模型和参数配置 |
| `GET` | `/api/home/tabs` | 否 | 获取首页 Tab |
| `GET` | `/api/home/{feed}` | 否 | 获取首页内容流 |
| `POST` | `/api/home/template/generate` | 是 | 使用首页模板和上传图片创建视频 |
| `GET` | `/api/app/update/check` | 否 | 检查 App 更新 |
| `POST` | `/api/user/register` | 否 | 邮箱注册 |
| `POST` | `/api/user/login` | 否 | 邮箱登录 |
| `POST` | `/api/user/login/app-code` | 否 | 使用网页生成的六位码登录 |
| `GET` | `/api/user/profile` | 是 | 获取用户资料 |
| `POST` | `/api/user/change-password` | 是 | 修改密码 |
| `POST` | `/api/user/delete-account` | 是 | 注销账号 |
| `GET` | `/api/payment/coins/package/list` | 否 | 金币套餐 |
| `GET` | `/api/payment/subscription/package/list` | 否 | 订阅套餐 |
| `GET` | `/api/payment/coins/balance` | 是 | 金币余额 |
| `POST` | `/api/payment/iap/verify` | 是 | 验证金币内购 |
| `POST` | `/api/payment/iap/verify-subscription` | 是 | 验证订阅内购 |
| `POST` | `/api/client/upload` | 否 | 上传生成输入文件 |
| `POST` | `/api/plugins/video/generate` | 是 | 提交图片或视频生成任务 |
| `GET` | `/api/plugins/video/query` | 是 | 查询生成任务 |
| `GET` | `/api/client/creations` | 是 | 获取当前用户近期创作记录 |
| `DELETE` | `/api/client/creations/{taskId}` | 是 | 删除一条已结束的创作记录 |
| `POST` | `/api/ads/facebook/events` | 是 | 上报服务端广告转化事件 |

`HEAD` 只用于基础连通性检查,部分接口由网关直接返回 200,不访问上游,不能用来判断业务接口是否健康。

## 5. 创作页与首页

### 5.1 GET /api/create-page/config

请求 Header:`X-Device-Context`。无 Query,无请求体。

成功响应主要结构:

```json
{
  "defaults": {
    "creationParams": {},
    "defaultTab": "imageToVideo",
    "defaultModelByTab": {}
  },
  "modes": {}
}
```

App 必须以接口返回的模型、参数、枚举和默认值渲染创作页,不要把模型清单写死在客户端。不同流量可能收到不同配置。

### 5.2 GET /api/home/tabs

无参数。响应:

```json
{
  "tabs": [
    {
      "name": "Trending",
      "endpoint": "/api/home/trending",
      "description": "热门列表"
    }
  ]
}
```

### 5.3 GET /api/home/{feed}

`feed` 可取 `trending`、`new`、`top_rated`、`most_favorited`、`short`。

| Query | 类型 | 必填 | 默认值 |
|-------|------|------|--------|
| `page` | integer | 否 | `1` |
| `perPage` | integer | 否 | `30` |

响应:

```json
{
  "feed": "trending",
  "page": 1,
  "perPage": 14,
  "total": 80,
  "maxReached": true,
  "data": [
    {
      "vid": "video-id",
      "title": "Example",
      "username": "Example",
      "modelName": "Example",
      "isPublic": true,
      "isVip": false,
      "price": 0,
      "keywords": [],
      "cover": "https://cdn.example.com/cover.jpg",
      "local_path": "https://cdn.example.com/video.mp4",
      "input_img": "",
      "modelRating": 5,
      "status": 2,
      "duration": 5,
      "config": {
        "coin": 0,
        "effectId": "",
        "templateType": ""
      }
    }
  ]
}
```

### 5.4 POST /api/home/template/generate

用途:App 将 Home Feed 返回的模板 `item` 和用户上传后的图片地址提交给网关。网关根据 `item` 选择受支持模型、构造模型参数,再调用现有视频生成服务。

```http
POST <APP_API_BASE_URL>/api/home/template/generate
```

请求 Header:

| Header | 必填 | 格式或说明 |
|--------|------|------------|
| `Authorization` | 是 | `Bearer <accessToken>` |
| `X-Site-Id` | 是 | 当前网站的站点 ID |
| `Content-Type` | 是 | `application/json` |
| `Accept` | 否 | 建议 `application/json` |
| `X-Locale` | 否 | 例如 `zh-CN` |
| `X-App-Version` | 否 | 例如 `1.0.0` |

请求字段:

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `item` | object | 是 | `/api/home/{feed}` 返回的用户所选完整条目 |
| `item.vid` | string | 是 | 模板或业务视频 ID |
| `item.local_path` | string | 广告 Home 模板必填 | 模板参考视频地址 |
| `item.duration` | number | 否 | 广告模板时长;缺失、非数字或不大于 0 时使用 `8` 秒 |
| `item.config` | object | 普通模板需要 | 普通模板配置 |
| `item.config.templateType` | string | 普通模板需要 | `dance`、`vidu` 或 `spicy` |
| `item.config.effectId` | string | 普通模板必填 | 模板效果 ID |
| `imageUrl` | string | 是 | `/api/client/upload` 上传后得到的、生成服务可访问的图片地址 |

请求示例:

```json
{
  "item": {
    "vid": "video-id",
    "local_path": "https://cdn.example.com/template.mp4",
    "duration": 8,
    "config": { "templateType": "vidu", "effectId": "effect-id" }
  },
  "imageUrl": "https://cdn.example.com/upload/person.jpg"
}
```

App 应直接传回 Home Feed 中用户点击的完整 `item`,不要只挑选部分展示字段重新组装。App 不传 `model`、`args`、`number` 或价格;即使在 `item` 中附带这些字段,网关也不会使用它们选择模型或计价。

模型选择规则:

| Home item 判定 | 上游模型 | 网关构造的主要参数 |
|----------------|----------|--------------------|
| `config.templateType=dance` | `aitubo_dance` | `sourceImage=imageUrl`、`danceId=config.effectId`、`faceEmotion=false`、`background=false` |
| `config.templateType=vidu` | `aitubo_template` | `effectId=config.effectId`、`images=[imageUrl]` |
| `config.templateType=spicy` | `sora_template` | `template_id=config.effectId`、`image=imageUrl`、`input_images=[imageUrl]` |
| 未提供 `config.templateType` | `mulerouter/w3.0-video` | `reference_images`、`reference_videos`、`template_video_id`、`duration`、`video_time` |

存在非空但不受支持的 `config.templateType` 时,网关拒绝请求,不会降级成广告模板。

完整 curl 示例:

```bash
curl -X POST \
  '<APP_API_BASE_URL>/api/home/template/generate' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'X-Site-Id: <SITE_ID>' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "item": {
      "vid": "video-id",
      "local_path": "https://cdn.example.com/template.mp4",
      "duration": 8,
      "config": {
        "templateType": "vidu",
        "effectId": "effect-id"
      }
    },
    "imageUrl": "https://cdn.example.com/upload/person.jpg"
  }'
```

普通 `vidu` 模板会被网关转换为:

```json
{
  "model": "aitubo_template",
  "video_id": "video-id",
  "number": 1,
  "args": {
    "effectId": "effect-id",
    "images": ["https://cdn.example.com/upload/person.jpg"]
  }
}
```

没有 `config.templateType` 的广告 Home 模板会被转换为:

```json
{
  "model": "mulerouter/w3.0-video",
  "video_id": "",
  "number": 1,
  "args": {
    "reference_images": ["https://cdn.example.com/upload/person.jpg"],
    "reference_videos": ["https://cdn.example.com/template.mp4"],
    "prompt": "Recreate Video 1 with the person from Image 1 as the primary subject.",
    "template_video_id": "video-id",
    "duration": 8,
    "video_time": 8,
    "preprocess": true,
    "skip_grok_prompt_optimize": true
  }
}
```

成功响应与 `/api/plugins/video/generate` 相同,网关不修改上游响应:

```json
{
  "code": 200,
  "data": {
    "task_id": "task-id",
    "model": "aitubo_template"
  }
}
```

App 必须保存 `data.task_id` 和 `data.model`,然后调用:

```http
GET /api/plugins/video/query?task_id=<TASK_ID>&model=<MODEL>
```

参数校验错误使用 HTTP 400:

| `error` | 常见原因 |
|---------|----------|
| `invalid_request` | JSON 无效,缺少 `item`、`imageUrl`、`item.vid`、普通模板 `effectId` 或广告模板 `local_path` |
| `unsupported_template_type` | `config.templateType` 不是 `dance`、`vidu`、`spicy` |

错误示例:

```json
{
  "error": "invalid_request",
  "message": "item.vid 不能为空"
}
```

App 完整调用流程:

1. 调用 `/api/home/{feed}` 获取 Home 模板。
2. 用户选择图片后调用 `/api/client/upload`。
3. 取得上传响应中的图片地址,并确认生成服务可以访问。
4. 将用户选择的完整 `item` 和图片地址传给 `/api/home/template/generate`。
5. 保存成功响应中的 `task_id` 和 `model`。
6. 调用 `/api/plugins/video/query` 轮询;`status=1` 继续等待,`status=2` 展示结果,`status=3` 停止并展示失败信息。

## 6. App 更新

### GET /api/app/update/check

请求必须带完整 `X-Device-Context.ap`。无 Query,无请求体。

有更新:

```json
{
  "updateAvailable": true,
  "forceUpdate": false,
  "release": {
    "version": "1.0.1",
    "buildNumber": 2,
    "forceUpdate": false,
    "storeUrl": "https://apps.apple.com/app/id0000000000",
    "downloadUrl": null,
    "updateUrl": "https://apps.apple.com/app/id0000000000",
    "updateKind": "store",
    "releaseNotes": "Bug fixes"
  }
}
```

无更新时 `release` 为 `null`。HTTP 404 且 `error=release_not_found` 表示服务端尚未为该平台和 Bundle ID 配置发版记录,不表示已经是最新版。

## 7. 用户与登录

### 7.1 POST /api/user/register

```json
{
  "email": "user@example.com",
  "password": "12345678",
  "nickname": "user"
}
```

| 字段 | 类型 | 必填 | 约束 |
|------|------|------|------|
| `email` | string | 是 | 合法邮箱 |
| `password` | string | 是 | 至少 6 个字符 |
| `nickname` | string | 否 | 用户昵称 |

成功 `data`:`accessToken`、`siteId`、`ver`。

### 7.2 POST /api/user/login

```json
{
  "email": "user@example.com",
  "password": "12345678"
}
```

成功 `data`:

```json
{
  "accessToken": "<token>",
  "ver": "1.0.0"
}
```

### 7.3 POST /api/user/login/app-code

网页端先为已登录用户生成六位码,App 再调用本接口。登录码必须用字符串保存,以保留前导零。

```json
{
  "loginCode": "003821"
}
```

| 字段 | 类型 | 必填 | 约束 |
|------|------|------|------|
| `loginCode` | string | 是 | 恰好六位 ASCII 数字,不能含空格 |

默认单 App 模式可省略 `X-App-Id`;多 App 模式必须带 `X-App-Id: <APP_ID>`。后端启用严格模式时还必须带与本次请求完全匹配的 `X-IOS-Proof`。

成功响应:

```json
{
  "code": 0,
  "info": "ok",
  "data": {
    "accessToken": "<new-token>",
    "ver": "",
    "iosLoginReward": {
      "granted": true,
      "amount": 500,
      "balance": 500
    }
  }
}
```

登录码有效期默认 5 分钟且只能成功兑换一次。网络失败后不要无限重试;如果服务端已提交事务,原码可能已经失效,应让用户重新获取。

### 7.4 GET /api/user/profile

请求 Header:`Authorization`、`X-Site-Id`。无 Query 和请求体。

`data` 字段:

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 用户 ID |
| `email` | string | 邮箱 |
| `nickname` | string | 昵称 |
| `avatarUrl` | string | 头像 URL |
| `premiumType` | integer | `0` 免费,`1` Premium |
| `onetimeSub` | integer | `1` 表示一次性订阅 |
| `premiumExpiresAt` | integer | 到期 Unix 秒时间戳 |
| `autoUnlock` | boolean | 是否自动解锁 |
| `status` | integer | 用户状态 |
| `pixelId` | string | 当前站点 Meta Pixel ID |
| `loginType` | integer | 登录方式 |
| `hasPurchased` | boolean | 是否有购买记录 |
| `ver` | string | App 版本信息 |

### 7.5 POST /api/user/change-password

请求 Header:`Authorization`、`X-Site-Id`。

```json
{
  "newPassword": "new-password"
}
```

`newPassword` 必填,长度 6~64 个字符。

### 7.6 POST /api/user/delete-account

请求 Header:`Authorization`、`X-Site-Id`。无 Query,请求体可为空。成功后 App 必须删除本地 Token 和用户缓存。

## 8. 支付与余额

### 8.1 GET /api/payment/coins/package/list

无请求体。可选 Query:`siteId`。建议仍显式发送 `X-Site-Id`。

`data[]`:`packageId`、`name`、`description`、`features`、`coinAmount`、`price`、`originalPrice`、`currency`、`discountPercentage`、`status`、`iosProductId`。

### 8.2 GET /api/payment/subscription/package/list

参数同金币套餐。`data[]`:`packageId`、`siteId`、`name`、`description`、`interval`、`price`、`originalPrice`、`currency`、`discountPercentage`、`coins`、`rights`、`status`、`iosProductId`、`createdAt`。

### 8.3 GET /api/payment/coins/balance

请求 Header:`Authorization`、`X-Site-Id`。可选 Query:`siteId`。

成功 `data`:

```json
{
  "balance": 500,
  "totalEarned": 500,
  "totalSpent": 0,
  "totalRealMoneySpent": 0
}
```

### 8.4 POST /api/payment/iap/verify

请求 Header:`Authorization`、`X-Site-Id`。

```json
{
  "transactionId": "2000000123456789",
  "account": "apple",
  "packageName": "com.example.app",
  "packageId": "server-package-id",
  "productId": "app-store-product-id"
}
```

| 字段 | 类型 | iOS 要求 | 说明 |
|------|------|----------|------|
| `transactionId` | string | 必填 | StoreKit 交易 ID |
| `account` | string | 必填 | iOS 使用 `apple` |
| `packageName` | string | 必填 | Bundle ID |
| `packageId` | string | 必填 | 网站数据库套餐 ID,不是 App Store Product ID |
| `productId` | string | 建议必填 | App Store Product ID |
| `purchaseToken` | string | Android 使用 | Google Play purchase token |

成功 `data.productId` 为已验证商品 ID。只有服务端验单成功后才能展示到账,不得以客户端支付回调代替服务端结果。

### 8.5 POST /api/payment/iap/verify-subscription

请求字段与金币验单相同。成功 `data`:

```json
{
  "isActive": true,
  "sandbox": false,
  "isInFreeTrial": false,
  "autoRenewStatus": true
}
```

## 9. 上传与生成

### 9.1 POST /api/client/upload

`Content-Type` 必须由 HTTP 库按 multipart boundary 自动生成,不要手写固定 boundary。

| Form 字段 | 类型 | 必填 | 说明 |
|-----------|------|------|------|
| `file` | file | 是 | 每次恰好一个文件 |

成功:

```json
{
  "code": 0,
  "info": "ok",
  "data": "res/user_upload/directory/file.jpg"
}
```

`data` 可能是相对存储路径。App 应按服务端约定拼成可公开访问 URL,再传给生成接口;上线验收时必须确认返回的是完整 URL 还是相对路径。

### 9.2 POST /api/plugins/video/generate

请求 Header:`Authorization`、`X-Site-Id`、`Content-Type: application/json`。

```json
{
  "model": "viduq2_image",
  "video_id": "template-1",
  "number": 1,
  "args": {
    "image": "https://cdn.example.com/input.jpg",
    "prompt": "跳舞",
    "duration": "5",
    "resolution": "720p"
  }
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `model` | string | 是 | 必须来自创作页配置 |
| `video_id` | string | 按模型 | 模板或业务视频 ID |
| `number` | integer | 否 | 默认 `1`,影响计价和生成数量 |
| `args` | object | 是 | 模型参数,必须按创作页配置构造 |

常见 `args` 字段包括 `prompt`、`negative_prompt`、`image`、`image_url`、`sourceImage`、`images`、`input_images`、`duration`、`resolution`、`aspect_ratio`、`ratio`、`movement_amplitude`、`generate_audio`、`effectId`、`danceId` 和 `input`。各模型字段不同,客户端不得假定全部通用。

成功:

```json
{
  "code": 0,
  "info": "ok",
  "data": {
    "task_id": "task-id",
    "model": "viduq2_image"
  }
}
```

保存 `task_id` 与 `model`,用于查询任务。

### 9.3 GET /api/plugins/video/query

| Query | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `task_id` | string | 是 | 生成接口返回的任务 ID |
| `model` | string | 建议 | 与生成请求一致的模型标识 |

主要 `data` 字段:`task_id`、`status`、`model`、`videos`、`images`、`error_msg`、`created_at`、`updated_at`。

`status`:`1` 处理中、`2` 成功、`3` 失败。处理中采用有上限的轮询与退避;成功或失败后立即停止。

## 10. 创作记录

### GET /api/client/creations

请求 Header:`Authorization`、`X-Site-Id`。

| Query | 类型 | 必填 | 默认值与限制 |
|-------|------|------|--------------|
| `page` | integer | 否 | 默认 `1` |
| `pageSize` | integer | 否 | 默认 `20`,最大 `100` |

成功:

```json
{
  "code": 0,
  "info": "ok",
  "data": {
    "items": [
      {
        "taskId": "task-id",
        "status": 2,
        "model": "viduq2_image",
        "prompt": "跳舞",
        "videos": [
          {
            "url": "https://cdn.example.com/result.mp4",
            "coverUrl": "https://cdn.example.com/cover.jpg"
          }
        ],
        "images": [],
        "createdAt": 1789430400,
        "updatedAt": 1789430460
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 20,
    "hasMore": false
  }
}
```

记录当前只保证最近 24 小时范围。用户 ID 从 JWT 获取,App 不传 `userId`。

### DELETE /api/client/creations/{taskId}

请求 Header:`Authorization: Bearer <accessToken>`、`X-Site-Id`、`Accept: application/json`。无请求体,`taskId` 使用列表返回的 `items[].taskId`。

仅允许删除 `status=2`(成功)或 `status=3`(失败)的任务;`status=1`(处理中)返回 HTTP 409。接口仅将记录从用户列表中隐藏,不删除任务、扣费和退款审计数据。任务不存在或不属于当前用户/站点时返回 HTTP 404,成功返回:

```json
{ "code": 0, "info": "ok", "data": {} }
```

curl 示例:

```bash
curl -X DELETE \
  '<APP_API_BASE_URL>/api/client/creations/<TASK_ID>' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'X-Site-Id: <SITE_ID>' \
  -H 'Accept: application/json'
```

Swift 示例:

```swift
func deleteCreation(taskId: String, accessToken: String, siteId: String) async throws {
    let encodedTaskId = taskId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? taskId
    let url = URL(string: "<APP_API_BASE_URL>/api/client/creations/\(encodedTaskId)")!
    var request = URLRequest(url: url)
    request.httpMethod = "DELETE"
    request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    request.setValue(siteId, forHTTPHeaderField: "X-Site-Id")
    request.setValue("application/json", forHTTPHeaderField: "Accept")

    let (data, response) = try await URLSession.shared.data(for: request)
    guard let http = response as? HTTPURLResponse else {
        throw URLError(.badServerResponse)
    }
    guard (200...299).contains(http.statusCode) else {
        let message = String(data: data, encoding: .utf8) ?? "删除失败"
        throw NSError(
            domain: "CreationAPI",
            code: http.statusCode,
            userInfo: [NSLocalizedDescriptionKey: message]
        )
    }
}
```

| HTTP | `code` | App 处理 |
|------|--------|----------|
| `200` | `0` | 从本地列表移除该任务,或重新请求创作列表 |
| `401` | `401` | Token 无效或过期,进入重新登录流程 |
| `404` | `404` | 记录不存在或不属于当前用户/站点,刷新列表 |
| `409` | `409` | 任务仍在处理中,提示 `只能删除已结束任务` |

App 不应对 `status=1` 的记录显示删除入口。删除成功只影响创作列表展示,不代表取消任务,也不会撤销扣费或退款。

## 11. Facebook 服务端事件

### POST /api/ads/facebook/events

请求 Header:`Authorization`、`X-Site-Id`。该接口由网关处理。

```json
{
  "eventName": "Lead",
  "eventId": "task-123",
  "eventTime": 1789430400,
  "eventSourceUrl": "https://example.com/",
  "customData": {},
  "userData": {
    "clientUserAgent": "...",
    "madid": "...",
    "anonId": "..."
  }
}
```

允许的 `eventName`:`PageView`、`Lead`、`InitiateCheckout`、`Purchase`。`Lead` 和 `Purchase` 必须提供稳定且唯一的 `eventId`。禁止由 App 提交 `pixelId`、Facebook Access Token、明文邮箱或测试事件码。

## 12. App 联调验收清单

- 使用新站 `SITE_ID` 注册和登录,Token 可访问 profile。
- 网页生成的六位码可在 App 登录一次,第二次兑换失败。
- 套餐的 `iosProductId` 与 App Store Connect 完全一致。
- 金币和订阅验单分别使用沙盒交易验证,重复提交不会重复发货。
- 上传真实图片后,返回路径可被生成服务访问。
- 图生视频提交成功,查询最终得到可播放 URL。
- 创作记录只返回当前 Token 用户和当前站点的数据。
- 新站 Bundle ID 存在发版记录,更新检查不会返回 `release_not_found`。
- 广告用户和自然用户分别验证创作页及首页内容。
- App 的日志和监控中不出现密码、Token、登录码、proof 或交易凭据。