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

# Webhook

> 投递负载、重试计划，以及如何编写安全的处理程序。

在任务上提供 `webhook.url`，当任务进入终止状态（`COMPLETED` 或 `FAILED`）时，我们会把结果 POST 到该地址。

```json theme={null}
{
  "taskType": "PERPLEXITY",
  "payload": { "prompt": "best wireless earbuds 2026" },
  "webhook": { "url": "https://your-server.example.com/hook" }
}
```

## 投递负载

我们以 `Content-Type: application/json` POST 如下正文：

<ResponseField name="task" type="object" required>
  与提交时拿到的任务元数据相同，此时带有终止 `status`，并回传你的 `idempotencyKey`。
</ResponseField>

<ResponseField name="credits" type="object" required>
  按引擎计算的额度消耗，例如 Perplexity 为 `2`、ChatGPT 为 `3`（视引擎为 1–3 额度）。任务失败时
  `creditsCharged` 为 `0`。与始终报告零的提交和轮询响应不同，这个字段是非零的。
</ResponseField>

<ResponseField name="response" type="object" required>
  引擎特定的结果，参见[引擎](/zh/engines/overview)。失败任务中，这里是 `{ "error": "<reason>" }`，而不是引擎结果。
</ResponseField>

```json theme={null}
{
  "task": {
    "id": "8f2c1e40-...",
    "taskType": "PERPLEXITY",
    "status": "COMPLETED",
    "priority": 1,
    "createdAt": "2026-07-09T04:12:00.000Z",
    "idempotencyKey": "job-42:prompt-7:perplexity"
  },
  "credits": { "creditsToCharge": 3, "creditsCharged": 3 },
  "response": {
    "text": "The best wireless earbuds in 2026 are ...",
    "sources": [{ "position": 1, "url": "https://...", "label": "..." }]
  }
}
```

失败的任务同样会投递：

```json theme={null}
{
  "task": { "id": "8f2c1e40-...", "status": "FAILED", "...": "..." },
  "credits": { "creditsToCharge": 3, "creditsCharged": 0 },
  "response": { "error": "ENGINE_TIMEOUT: The engine did not answer before the task deadline." }
}
```

## 重试

返回任意 `2xx` 即表示确认。其他任何情况（包括超时）都算失败，我们会以指数退避重试：

| 尝试 | 触发时间 |
| - | - |
| 1 | 立即 |
| 2 | 2 分钟后 |
| 3 | 4 分钟后 |
| 4 | 8 分钟后 |
| 5 | 16 分钟后 |

第五次尝试后，投递会被放弃并记录日志。没有可供读取的死信队列。如需通过任务 API 获取结果，请在 24 小时公开
轮询窗口内轮询。该窗口并不决定底层记录的存储期限。

每次尝试的请求超时为 **30 秒**。

<Warning>
  从任务的角度看，投递是发出即不管的。任务已完成但 webhook 始终无法投递时，`GET /v1/async/task/{id}` 仍显示
  `COMPLETED`。状态描述的是引擎工作，而不是通知。
</Warning>

## 编写安全的处理程序

<Steps>
  <Step title="快速确认">
    一旦把负载持久化入队，立即返回 `200`。解析和数据库写入放在之后。处理缓慢会耗尽 30 秒超时，招来你不想要的重试。
  </Step>

  <Step title="按 idempotencyKey 去重">
    由于存在重试，处理程序多次收到同一任务是正常的。按 `task.idempotencyKey`（或 `task.id`）匹配，并让写入幂等。
  </Step>

  <Step title="按状态分支，而非按字段是否存在">
    显式检查 `task.status === "FAILED"`。失败的任务同样会投递 webhook。
  </Step>
</Steps>

<Note>
  Webhook 请求不带签名或共享密钥。如果你的端点是公开的，请将负载视为不可信，使用难以猜测的 URL 路径，
  或将处理程序置于网络层访问限制之后。
</Note>
