# Webhooks

**Base URL:** `https://openapi.tripo3d.ai/v3`

**Endpoint:** `POST https://<your-endpoint>`

当任务完成或失败、或积分余额低于你在账单中的告警阈值时，Tripo 会向你的 HTTPS URL 投递 JSON，因此无需轮询 `GET /v3/tasks/{task_id}`。配置流程
- 打开 **设置 → Webhooks**，点击 **添加端点**。
- 填写 HTTPS 回调地址（例如 `https://api.yourapp.com/tripo-webhook`）。
- 选择要订阅的事件类型。
- 点击 **保存端点**。
- 复制 **签名密钥**（`whsec_…`）——仅显示一次，请像 API Key 一样妥善保管。

事件类型
- `task.completed`：任务成功结束。
- `task.failed`：任务失败或已取消。
- `balance.low`：余额低于在 **账单 → 额度告警**中设定的阈值（告警本身不决定任务是否成功）。

重试策略若你的端点返回非 2xx 或超时，Tripo 会自动重试。可在 设置 → Webhooks → 最近投递记录 中排查失败原因。
最佳实践
- 处理业务前务必用签名密钥验证载荷来源。
- 约 5 秒内返回 `2xx`；重逻辑异步处理。
- 使用 `Tripo-Webhook-Delivery` 做投递去重。
- 在 账单 中开启 `balance.low` 以便余额不足前收到通知。




## Request Parameters

### id

- **Type:** string
- **Required:** 必选

组合后的事件 ID（通常包含 task_id 与事件类型）。
### type

- **Type:** string
- **Required:** 必选

取值为 `task.completed`、`task.failed`、`balance.low` 之一。
### created_at

- **Type:** string
- **Required:** 必选

事件创建时间，ISO 8601 字符串。
### data

- **Type:** object
- **Required:** 必选

事件相关载荷。任务类事件中的字段与任务查询接口返回一致；balance.low 时包含与余额相关的字段。


- 任务事件：通常包含 `task_id`、`type`、`status`，成功时含 `output`。
- `balance.low`：阈值在 账单 → 额度告警 中配置。

### Tripo-Webhook-Id

- **Type:** header · string
- **Required:** 必选

Tripo 侧 Webhook 端点 ID。
### Tripo-Webhook-Delivery

- **Type:** header · string
- **Required:** 必选

唯一投递 ID——若同一逻辑事件多次投递，可据此去重。
### Tripo-Webhook-Event

- **Type:** header · string
- **Required:** 必选

与 JSON 体中的 type 一致。
### Tripo-Webhook-Signature

- **Type:** header · string
- **Required:** 必选

使用签名密钥对 `{时间戳}.{原始请求体}` 做 HMAC-SHA256。格式： `t=<unix>,v1=<hex>`。


- 从请求头解析 `t` 与 `v1`。
- 以密钥计算 HMAC-SHA256(`t + "." + 原始 body`)，小写十六进制。
- 与 `v1` 做常量时间比较。
- 可选：若 t 与当前时间相差超过约 5 分钟则拒绝（防重放）。

### Content-Type

- **Type:** header · string
- **Required:** 必选
- **Default:** `application/json`

固定为 application/json。

## Response Fields

### HTTP

- **Type:** status
- **Required:** 必选

应尽快返回 2xx（数秒内）。在重计算完成前即可先响应。

## Request Example

### task.completed

```
{
  "id": "task_abc123:task.completed",
  "type": "task.completed",
  "created_at": "2026-05-06T12:00:00Z",
  "data": {
    "task_id": "task_abc123",
    "type": "text_to_model",
    "status": "success",
    "output": {
      "model_url": "https://cdn.tripo3d.ai/output/model_pbr.glb"
    }
  }
}
```

### balance.low

```
{
  "id": "evt_balance_low:balance.low",
  "type": "balance.low",
  "created_at": "2026-05-06T12:00:00Z",
  "data": {
    "balance": 1200,
    "threshold": 5000
  }
}
```

### 验证 · Python

```
import hmac
import hashlib
import time

def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    timestamp = parts.get("t", "")
    expected_sig = parts.get("v1", "")

    if abs(time.time() - int(timestamp)) > 300:
        return False

    computed = hmac.new(
        secret.encode("utf-8"),
        f"{timestamp}.{payload.decode('utf-8')}".encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(computed, expected_sig)
```

### 验证 · JavaScript

```
const crypto = require("crypto");

function verifyWebhook(payload, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map(p => p.split("=", 2))
  );
  const timestamp = parts.t;
  const expectedSig = parts.v1;

  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
    return false;
  }

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

  return crypto.timingSafeEqual(
    Buffer.from(computed),
    Buffer.from(expectedSig)
  );
}
```


## Response Example

### Tripo-Webhook-Signature

```json
Tripo-Webhook-Signature: t=1714992000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9
```

### task.failed

```json
{
  "id": "task_abc123:task.failed",
  "type": "task.failed",
  "created_at": "2026-05-06T12:00:05Z",
  "data": {
    "task_id": "task_abc123",
    "type": "text_to_model",
    "status": "failed",
    "error": {
      "code": "task_failed",
      "message": "Generation failed"
    }
  }
}
```
