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

# 设置 Callback 或 Webhook Endpoint

> 在 WaaS 2.0 中设置 Callback 或 Webhook Endpoint 的分步指南，用于处理实时事件通知。

<Tip>
  即刻安装 [Cobo WaaS Skill](/developers/v2_cn/guides/overview/cobo-waas-skill)，在 Claude Code、Cursor 等 AI 开发环境中使用自然语言集成 WaaS API，显著提升开发效率 🚀
</Tip>

Webhook 事件和 Callback 消息对于确保 WaaS 服务与您的 App 之间的无缝数据集成和通信至关重要。本指南介绍如何搭建一个用于接收和处理 Webhook 事件和 Callback 消息的 Endpoint。

## 创建 Endpoint

首先，选择一个支持接收和处理 Webhook 事件或 Callback 消息的服务器环境，例如 AWS、Google Cloud 或自托管服务器。然后，在您的服务器上定义一个 Endpoint URL，以便接收 Webhook 事件和 Callback 消息。

## 实现处理逻辑

创建 Endpoint 后，您需要在服务器上实现逻辑以处理 Webhook 事件或 Callback 消息，包括解析 API 请求、验证签名、响应请求以及在必要时添加其他处理逻辑。

### 验证签名

为了防止未经授权的访问，当您收到 Webhook 事件或 Callback 消息时，您需要通过验证签名来验证 API 请求的真实性。

<Warning>
  Ed25519 签名是基于 HTTP 请求包体的原始字节精确计算的。您必须使用收到的原始包体（raw body）来验证签名。请勿先解析 JSON 再重新序列化后进行验证——重新编码可能会去除 `null` 或空字段并重新排列键的顺序，从而改变字节内容并导致签名验证失败。请始终使用未经修改的原始包体，将消息构造为 `raw_body|timestamp`。
</Warning>

验证步骤如下：

1. 获取请求包体的原始数据和时间戳。

   从请求 Payload 中提取包体的原始字符串，并从请求头中提取时间戳。

   ```python theme={null}
   raw_body = request.body().decode('utf8')
   timestamp = request.headers.get("BIZ_TIMESTAMP")
   ```

2. 获取签名。

   从请求头中获取签名值。

   ```python theme={null}
   signature = request.headers.get('BIZ_RESP_SIGNATURE')
   ```

3. 拼接消息并对其进行哈希处理。

   ```python theme={null}
   import hashlib

   # Concatenate raw body and timestamp to form the message.
   message = "raw_body|timestamp"

   # Compute double SHA-256 hash.
   sha256_hash = hashlib.sha256(hashlib.sha256(message.encode()).digest()).digest()
   ```

4. 选择 Cobo 的公钥。

   根据您使用的环境，选择相应的公钥进行验证：

   * 开发环境：`a04ea1d5fa8da71f1dcfccf972b9c4eba0a2d8aba1f6da26f49977b08a0d2718`
   * 生产环境：`8d4a482641adb2a34b726f05827dba9a9653e5857469b8749052bf4458a86729`

5. 使用 Ed25519 算法验证签名。

   ```python theme={null}
   import ed25519

   # Obtain the verifying key from Cobo's public key.
   vk = ed25519.VerifyingKey(bytes.fromhex(public_key)) 

   # Verify the signature against the computed message hash.
   vk.verify(bytes.fromhex(signature), sha256_hash)
   ```

### 响应 API 请求

正确响应 Webhook 事件和 Callback 消息对于确保 Webhook 和 Callback 按预期处理至关重要。本节描述了 Webhook 和 Callback Endpoint 的预期响应。

#### Webhook 事件

当您的 Webhook Endpoint 收到 Webhook 事件时，它应该响应状态码 `200` 或 `201` 以指示事件已成功接收和处理。一旦发送此响应，WaaS 服务将停止重试发送事件，事件状态将变为**已送达**。

默认情况下，每个 Webhook 事件的超时时间为 2 秒。如果 Webhook Endpoint 没有响应或响应状态码不是 `200` 或 `201`，WaaS 服务将继续重试发送事件。如果重试次数达到 10 次，WaaS 服务将停止发送事件，事件状态将变为**发送失败**。您可以通过单击 Cobo Portal > 开发者 > WaaS 2.0 > Webhook 事件上的**重新发送**来重新发送事件。

Cobo 不保证事件将按生成顺序交付。例如，创建转账将生成以下事件：

* `wallets.transaction.created`
* `wallets.transaction.updated`
* `wallets.transaction.succeeded`

您的 Endpoint 不应假设事件将按此顺序到达。

#### Callback 消息

当您的 Callback Endpoint 收到 Callback 消息时，它应该响应状态码 `200` 或 `201` 和响应包体 `ok` 或 `deny` 以指示交易批准或拒绝。一旦发送此响应，WaaS 服务将停止重试发送消息，Callback 消息状态将变为**已送达**。

如果 Callback Endpoint 没有响应，响应状态码不是 `200` 或 `201`，或响应包体不包含 `ok` 或 `deny`，WaaS 服务将继续重试发送消息。如果重试次数达到 30 次，WaaS 服务将停止发送消息，Callback 消息状态将变为**发送失败**。您可以使用 [Retry callback message](/developers/v2/api-references/developers/retry-callback-message) 来重新发送消息。

### 常见投递失败原因

下表列出了 Callback 消息和 Webhook 事件最常见的投递失败原因及修复方法。

| 现象                                                                      | 根因                                 | 修复方法                                                                                  |
| ----------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------- |
| Callback 消息状态为`重试中`或`发送失败`，Portal 日志中响应包体显示 `{"result":"ok"}` 等 JSON 格式 | 响应包体是 JSON 而非纯文本                   | 将响应包体改为纯文本字面量 `ok` 或 `deny`，不要包 JSON、不要加引号。                                           |
| 新注册的 Callback URL 没有任何投递记录                                              | Endpoint 不可公开访问（如 ngrok 隧道未运行或已重启） | 启动或重启 ngrok 隧道，确认 URL 在本地网络外可正常访问后再注册。                                                |
| Webhook 事件投递日志中 `status_code` 为 `301` 或 `302`                           | 服务器返回了重定向                          | 直接在注册的 URL 上返回响应，Cobo 不跟随重定向。                                                         |
| 投递日志显示 TLS 或 SSL 错误                                                     | Endpoint 的 TLS 证书无效或已过期            | 在您的服务器上更新证书，Endpoint 必须使用有效的 HTTPS 证书。                                                |
| 事件或消息在一段时间后停止投递                                                         | 重试次数已耗尽                            | Webhook 事件最多重试 10 次，Callback 消息最多重试 30 次。超限后状态变为**发送失败**，请在 Cobo Portal 或通过 API 手动重试。 |

<Note>
  您可以在 Cobo Portal 的**开发者** > **Webhook 事件** / **Callback 消息** > 点击某条记录 > **投递日志**中查看每次投递的响应包体和状态码。
</Note>

### 代码示例

要查看如何实现处理逻辑的示例，请参阅 WaaS SDK GitHub 库中的如下文件：

* Python：[server\_demo.py](https://github.com/CoboGlobal/cobo-waas2-python-sdk/blob/master/cobo_waas2/server_demo.py)（基于 FastAPI 框架实现）
* Java：[DemoController.java](https://github.com/CoboGlobal/cobo-waas2-java-sdk/blob/master/src/main/java/com/cobo/waas2/demo/DemoController.java)（基于 SpringBoot 框架实现）
* JavaScript：[ServerDemo.js](https://github.com/CoboGlobal/cobo-waas2-js-sdk/blob/master/src/ServerDemo.js)
* Go：[server\_demo.go](https://github.com/CoboGlobal/cobo-waas2-go-sdk/blob/master/cobo_waas2/demo/server_demo.go)

## 进阶用法

### 使用钱包级别的 Webhook 分发

在某些业务场景中，您可能需要为不同的钱包设置不同的 Webhook 处理逻辑。例如：

* 多个业务团队共用同一个 WaaS 账户
* 一个系统管理多个独立项目
* 需要将事件转发到不同的微服务

在这种情况下，您可以在 Webhook 处理逻辑中根据 `wallet_id` 执行分发。以下示例展示了如何基于 Cobo 提供的 Webhook/Callback 示例代码（[server\_demo.py](https://github.com/CoboGlobal/cobo-waas2-python-sdk/blob/master/cobo_waas2/server_demo.py)），实现按钱包 ID 分发 Webhook 的逻辑。示例使用 Python 语言演示，其他语言的实现方式类似，您可以参考此逻辑并将其加入到您所使用语言的示例代码中。

```python theme={null}
@app.post("/api/webhook")
async def handle_webhook(
    request: Request,
    biz_timestamp: Optional[str] = Header(None),
    biz_resp_signature: Optional[str] = Header(None),
):
    raw_body = await request.body()
    sig_valid = verify_signature(
        pubkey, biz_resp_signature, f"{raw_body.decode('utf8')}|{biz_timestamp}"
    )
    if not sig_valid:
        raise HTTPException(status_code=401, detail="Signature verification failed")

    import requests
    event = WebhookEvent.from_dict(json.loads(raw_body.decode('utf8')))
    data = event.data.actual_instance

    # 根据不同的钱包 ID，将事件分发到不同的业务系统
    # 请将下面的 wallet_id_xxx 和 URL 替换为您的实际钱包 ID 和 webhook endpoint
    if data.data_type == "Transaction":
        if data.wallet_id == "wallet_id_1":
            requests.post("http://wallet1.example.com/webhook", json=data.model_dump())
        elif data.wallet_id == "wallet_id_2":
            requests.post("http://wallet2.example.com/webhook", json=data.model_dump())
        elif data.wallet_id == "wallet_id_3":
            requests.post("http://wallet3.example.com/webhook", json=data.model_dump())
        else:
            # 默认处理逻辑（可选）
            pass

    logger.info(event)
    logger.info(event.data)
```

## ⚠️⚠️⚠️ 注意事项

<Warning>
  * 在接收 Webhook 事件时，您的 Endpoint 应该首先快速返回正确的状态码，然后异步处理任何后续处理，以防止超时。
  * 由于 Webhook 事件存在重试机制，Webhook Endpoint 有时会多次接收到相同的事件。为了防止重复事件处理，请记录已处理的 Event ID，Transaction Hash 或 Transaction ID，并避免处理已记录的事件。
</Warning>
