# 回调（Webhook）

### 概览

当平台发生事件时，会通过 HTTP `POST` 调用你配置的回调地址（callbackUrl）。你需要：

1. 接收并解析 JSON 数据；
2. 根据 `X-EVENT-TYPE` 识别事件类型；
3. 成功处理后 **返回 `HTTP 200`** ；
4. 否则平台将按重试策略重新通知。

***

### 配置回调地址

* **入口位置**：进入 **用户中心 → API 设置** 页面。
* **配置项**：填写 `callbackUrl` 并勾选要订阅的事件类型。
* **协议**：支持 **HTTP** 或 **HTTPS**。
* **路径**：平台通过请求头 `X-EVENT-TYPE` 标识事件类型。
* **不可通过 API 动态订阅**。

***

### 请求头说明

平台每次回调都会附带以下 HTTP 头：

| Header 名称         | 描述                 |
| ----------------- | ------------------ |
| `X-EVENT-ID`      | 事件唯一 ID（幂等键）       |
| `X-EVENT-TYPE`    | 事件类型（枚举 EventType） |
| `X-EVENT-VERSION` | 事件版本号              |

***

### 事件结构（JSON）

平台推送的回调请求体为 **JSON**，顶层字段采用 **snake\_case**：

```json
{
  "event_type": "EVENT_BALANCE",
  "event_id": "aabbccdd-1122-3344-5566-77889900",
  "data": { /* 事件对应的数据体 */ }
}
```

#### 事件类型（EventType）

当前有效事件类型：`EVENT_BALANCE`、`EVENT_TRON_MATE_SUBSCRIPTION`、`EVENT_DELEGATION`（如收到未知类型，请记录并忽略）。

<table data-full-width="true"><thead><tr><th>事件类型</th><th>事件名称</th><th>触发场景</th><th>回调数据结构</th><th>说明与处理</th></tr></thead><tbody><tr><td><strong><code>EVENT_BALANCE</code></strong></td><td>余额变动事件（Balance Changed）</td><td>当用户账户余额发生变化（充值、提现、质押收益、消费、能量支出等）</td><td><code>BalanceChangeEventPayload</code>包含字段：<code>payment_hash</code>、<code>balance_type</code>、<code>billing_type</code>、<code>coin_type</code>、<code>amount_sun</code>、<code>balance</code>、<code>balance_usdt</code>、<code>timestamp</code>、<code>remark</code></td><td>用于通知账户余额的实时变化；可据此更新资金记录或展示统计。</td></tr><tr><td><strong><code>EVENT_TRON_MATE_SUBSCRIPTION</code></strong></td><td>波场伴侣订阅事件（Tron Mate Subscription）</td><td>订阅 CatFee Tron Mate 服务（BASIC / PRO 等）成功时触发</td><td><code>TronMateSubscriptionEventPayload</code>包含字段：<code>payment_hash</code>、<code>payment_amount_sun</code>、<code>payment_timestamp</code>、<code>subscribe_type</code>、<code>address</code></td><td>通知有新订阅发生；每个地址每24小时触发一次。</td></tr><tr><td><strong><code>EVENT_DELEGATION</code></strong></td><td>代理确认事件（Delegation Confirmed）</td><td>用户能量(或带宽)代理确认上链后触发</td><td><code>DelegationEventPayload</code>包含字段：<code>order_id</code>、<code>resource_type</code>、<code>delegation_type</code>、<code>receiver</code>、<code>payment_amount_sun</code>、<code>payment_timestamp</code>、<code>duration</code>、<code>quantity</code>、<code>staked_sun</code>、<code>delegation_hash</code>、<code>delegation_timestamp</code></td><td>通知能量(或带宽)代理确认成功，可用于账单记录同步。</td></tr></tbody></table>

#### 示例：余额变动（EVENT\_BALANCE）

```json
{
  "event_type": "EVENT_BALANCE",
  "event_id": "aabbccdd-1122-3344-5566-77889900",
  "data": {
    "balance_type": "BALANCE_CHANGE_TRANSFER",
    "billing_type": "BILLING_ENERGY",
    "coin_type": "USDT",
    "amount_sun": 1000000,
    "balance": 500000000,
    "balance_usdt": 2000000,
    "timestamp": 1760505600,
    "remark": "transfer in"
  }
}
```

#### 示例：订阅（EVENT\_TRON\_MATE\_SUBSCRIPTION）

```json
{
  "event_type": "EVENT_TRON_MATE_SUBSCRIPTION",
  "event_id": "22334455-6677-8899-aabb-ccddeeff",
  "data": {
    "payment_amount_sun": 5000000,
    "payment_timestamp": 1760505600,
    "subscribe_type": "SUBSCRIBE_PRO",
    "address": "TGxxx..."
  }
}
```

***

### 回调请求格式

* **方法**：`POST`
* **Content-Type**：`application/json; charset=utf-8`
* **请求头**：包含 `X-EVENT-ID`、`X-EVENT-TYPE`、`X-EVENT-VERSION`

#### 示例请求

```http
POST /callback HTTP/1.1
Host: example.com
Content-Type: application/json
X-EVENT-ID: aabbccdd-1122-3344-5566-77889900
X-EVENT-TYPE: EVENT_BALANCE
X-EVENT-VERSION: 2025-01-01

{
  "event_type": "EVENT_BALANCE",
  "event_id": "aabbccdd-1122-3344-5566-77889900",
  "data": {
    "balance_type": "BALANCE_CHANGE_TRANSFER",
    "billing_type": "BILLING_ENERGY",
    "coin_type": "USDT",
    "amount_sun": 1000000,
    "balance": 500000000,
    "balance_usdt": 2000000,
    "timestamp": 1760505600,
    "remark": "transfer in"
  }
}
```

### 事件数据定义：

## The DelegationEvent object

```json
{"openapi":"3.1.0","info":{"title":"Notifier API","version":"0.1#@BUILD_ID@"},"components":{"schemas":{"DelegationEvent":{"properties":{"event_type":{"type":"string","description":"事件类型","enum":["EVENT_UNKNOWN","EVENT_BALANCE","EVENT_TRON_MATE_SUBSCRIPTION","EVENT_DELEGATION","EVENT_INTERNAL_MESSAGE","UNRECOGNIZED"]},"event_id":{"type":"string","description":"订单ID，幂等"},"data":{"$ref":"#/components/schemas/DelegationEventData"}}},"DelegationEventData":{"description":"代理详细数据","properties":{"resource_type":{"type":"string","description":"资源类型：ENERGY | BANDWIDTH","enum":["ENERGY","BANDWIDTH","UNRECOGNIZED"]},"delegation_type":{"type":"string","description":"代理类型","enum":["DELEGATION_UNKNOWN","DELEGATION_NORMAL","DELEGATION_MATE_SLOT","DELEGATION_MATE_REPLENISH","UNRECOGNIZED"]},"receiver":{"type":"string","description":"接收地址"},"payment_amount_sun":{"type":"integer","format":"int64","description":"付款金额"},"payment_timestamp":{"type":"integer","format":"int64","description":"付款时间"},"duration":{"type":"integer","format":"int32","description":"周期（分钟）"},"quantity":{"type":"integer","format":"int32","description":"数量"},"staked_sun":{"type":"integer","format":"int64","description":"质押的trx"},"delegation_hash":{"type":"string","description":"代理hash"},"delegation_timestamp":{"type":"integer","format":"int64","description":"代理时间"}}}}}}
```

## The BalanceEvent object

```json
{"openapi":"3.1.0","info":{"title":"Notifier API","version":"0.1#@BUILD_ID@"},"components":{"schemas":{"BalanceEvent":{"description":"余额变动事件","properties":{"event_type":{"type":"string","description":"事件类型","enum":["EVENT_UNKNOWN","EVENT_BALANCE","EVENT_TRON_MATE_SUBSCRIPTION","EVENT_DELEGATION","EVENT_INTERNAL_MESSAGE","UNRECOGNIZED"]},"event_id":{"type":"string","description":"订单ID，幂等"},"data":{"$ref":"#/components/schemas/BalanceEventData"}}},"BalanceEventData":{"description":"账户变动信息","properties":{"balance_type":{"type":"string","description":"余额类型","enum":["BALANCHE_CHANGE_RECHARGE","BALANCE_CHANGE_TRANSFER","BALANCE_CHANGE_DAPP","BALANCE_CHANGE_BALANCE","BALANCE_CHANGE_API","BALANCE_CHANGE_BOT","BALANCE_CHANGE_WITHDRAW","BALANCE_CHANGE_REFUND","UNRECOGNIZED"]},"billing_type":{"type":"string","description":"业务计费类型","enum":["UNKNOWN_BILLING_TYPE","BILLING_ENERGY","BILLING_BANDWIDTH","BILLING_PREMIUM","BILLING_NODE","BILLING_MONITOR","BILLING_DAY_ENERGY","BILLING_MULTIDAY_ENERGY","BILLING_FLASH","BILLING_SUBSCRIBE","BILLING_PERMISSION","BILLING_MARGIN","UNRECOGNIZED"]},"coin_type":{"type":"string","description":"币种","enum":["USDT","TRX","ETH","BTC","BNB","BUSD"]},"amount_sun":{"type":"integer","format":"int64","description":"金额sun"},"balance":{"type":"integer","format":"int64","description":"trx余额"},"balance_usdt":{"type":"integer","format":"int64","description":"usdt余额"},"timestamp":{"type":"integer","format":"int64","description":"时间"},"remark":{"type":"string","description":"备注"}}}}}}
```

## The TronMateSubscriptionEvent object

```json
{"openapi":"3.1.0","info":{"title":"Notifier API","version":"0.1#@BUILD_ID@"},"components":{"schemas":{"TronMateSubscriptionEvent":{"description":"波场伴侣订阅事件","properties":{"event_type":{"type":"string","description":"事件类型","enum":["EVENT_UNKNOWN","EVENT_BALANCE","EVENT_TRON_MATE_SUBSCRIPTION","EVENT_DELEGATION","EVENT_INTERNAL_MESSAGE","UNRECOGNIZED"]},"event_id":{"type":"string","description":"订单ID，幂等"},"data":{"$ref":"#/components/schemas/TronMateSubscriptionEventData"}}},"TronMateSubscriptionEventData":{"description":"订阅详细数据","properties":{"payment_amount_sun":{"type":"integer","format":"int64","description":"支付金额"},"payment_timestamp":{"type":"integer","format":"int64","description":"支付时间"},"subscribe_type":{"type":"string","description":"订阅类型(BASIC|PRO)","enum":["SUBSCRIBE_UNKNOWN","SUBSCRIBE_BASIC","SUBSCRIBE_PRO","UNRECOGNIZED"]},"address":{"type":"string","description":"地址"}}}}}}
```

***

### 返回要求

> ⚠️ **NOTICE:** Please return success to notify the notification server after successfully receiving the callback request. After the notification server receives success, the notification stops.

* 成功接收后，**必须**返回：

  ```http
  HTTP/1.1 200 OK
  Content-Type: text/plain; charset=utf-8

  ```
* 平台以返回 `200` 为成功，否则将重试。

***

### 重试机制

若响应非 200 ，系统会进行重试。

#### 重试次数与间隔

共 **10 次通知**，间隔如下：

```
0s / 15s / 30s / 3m / 10m / 20m / 30m / 60m / 3h / 6h
```

若连续 10 次均失败，则系统放弃并记录。

***

### 幂等处理建议

* 请使用 `X-EVENT-ID` 作为幂等键。
* 对于已处理事件，应直接返回 `200` 。

***

### 示例代码

#### Node.js (Express)

```js
import express from 'express';
const app = express();
app.use(express.json());
const processed = new Set();

app.post('/callback', (req, res) => {
  const eventId = req.header('X-EVENT-ID');
  if (processed.has(eventId)) return res.status(200).type('text/plain').send('success');
  processed.add(eventId);

  const evt = req.body; // { event_type, data }
  // TODO: 处理事件

  res.status(200).type('text/plain').send('success');
});

app.listen(8080);
```

#### Java (Spring Boot)

```java
@RestController
public class WebhookController {
  private final Set<String> processed = Collections.synchronizedSet(new HashSet<>());

  @PostMapping(value = "/callback", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
  public ResponseEntity<String> handle(@RequestHeader("X-EVENT-ID") String id,
                                       @RequestBody Map<String, Object> evt) {
    synchronized (processed) {
      if (processed.contains(id)) return ResponseEntity.ok("success");
      processed.add(id);
    }

    // TODO: 事件处理逻辑

    return ResponseEntity.ok("success");
  }
}
```

#### Python (FastAPI)

```python
from fastapi import FastAPI, Request, Header, Response
app = FastAPI()
processed = set()

@app.post("/callback")
async def webhook(req: Request, x_event_id: str = Header(None)):
    if x_event_id in processed:
        return Response(content="success", media_type="text/plain")
    processed.add(x_event_id)

    evt = await req.json()
    # TODO: 事件处理

    return Response(content="success", media_type="text/plain")
```

***

### FAQ

**Q1：返回 JSON 可以吗？** 可以。只要返回的HTTP状态为200即可。

**Q2：HTTP 支持吗？** 支持 HTTP 与 HTTPS。

**Q3：可否动态订阅？** 不支持，仅能在用户中心配置。

**Q4：如何分发不同事件？** 根据请求头 `X-EVENT-TYPE` 实现逻辑分支。

**Q5：事件会乱序吗？** 系统尽量保证同类事件顺序，但不作绝对保证。

**Q6：事件版本如何处理？** 根据 `X-EVENT-VERSION` 选择解析方式，未知字段请忽略。

***

**版本：v1.1.1（2025-10-21）**


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.catfee.io/getting-started/webhook.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
