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

# Webhook 配置

> 说明如何登记回调地址、用 sortedCompactJson 复算并校验 X-Signature、按 webhookId 幂等去重，以及投递与重试的时间窗约定。

## 📄 正文

企业卡的组织开户、员工创建、建卡、交易与入金都是异步的，终态由平台在事件发生时主动推送到您登记的回调地址，您不必轮询。接收端只需实现**一个**接口——所有事件共用同一套信封，读 `webhookType` 再解析 `data` 即可。事件全集与各事件的 `data` 结构见[事件与数据结构](./events-and-schema)。

## 一、登记回调地址

回调地址由平台运营为您配置，没有自助修改接口，需变更请联系对接人。**沙盒与生产是两套独立配置**，须分别登记。

<Warning>
  未配置回调地址时，需要您参与的链路会直接失败——3DS 挑战无法转发给您，交易会被拒绝。走 OOB 或 `otpSendMode=ENTERPRISE` 发码前，必须先把回调地址配好。
</Warning>

### 回调地址要求

| 要求 | 说明                                            |
| :- | :-------------------------------------------- |
| 协议 | 必须是 HTTPS，证书须可公网校验                            |
| 方法 | 接收 `POST`，请求体为 JSON                           |
| 响应 | 返回 HTTP `2xx` 即视为投递成功，平台**不解析响应体**            |
| 时延 | 尽快返回——请先落库再异步处理业务，不要在回调里做长耗时操作                |
| 幂等 | 同一 `webhookId` 可能被推送多次，须按它去重；**去重后仍返回 `2xx`** |

## 二、校验签名（HMAC-SHA256）

每一条推送都带 `X-Signature`。签名算法是：

```
X-Signature = Hex( HMAC-SHA256( SK, sortedCompactJson(整个 payload) ) )
```

`SK` 是平台为您单独下发的 Webhook 签名密钥，按入驻方隔离，与 API 请求签名的密钥不是同一把。

### 请求头

| 头             | 说明                 |
| :------------ | :----------------- |
| `X-Signature` | 签名值，十六进制小写         |
| `Accept`      | `application/json` |

事件类型、时间戳与 ID 全部在请求体里，**不进请求头**。

<Warning>
  **不能直接对原始字节验签。** `sortedCompactJson` 的含义是：把 payload **所有层级**的字段按字段名字典序排序，再去掉空白做紧凑序列化。您必须用同样的方式重新序列化后再比对，否则签名一定不匹配。
</Warning>

### 校验示例

```python theme={null}
import hmac, hashlib, json

def sorted_compact_json(obj):
    # 所有层级按字段名字典序排序 + 去空白紧凑序列化
    return json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False)

def verify(payload_dict: dict, header_signature: str, sk: str) -> bool:
    body = sorted_compact_json(payload_dict).encode('utf-8')
    expected = hmac.new(sk.encode('utf-8'), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_signature.lower())
```

验签失败请直接丢弃并告警，不要按业务处理。

## 三、投递、重试与顺序

| 项    | 约定                                                       |
| :--- | :------------------------------------------------------- |
| 成功判定 | HTTP `2xx` 即成功，不解析响应体                                    |
| 重试触发 | 非 `2xx` 或超时                                              |
| 重试节奏 | 落库待重投，定时任务约每分钟扫描一轮；重投上限 **3 次**、间隔约 1 分钟、**无指数退避**       |
| 总窗口  | 首发加重投合计最多 **4 次**，约 **3 分钟**内结束                          |
| 仍失败  | 平台侧告警并人工跟进                                               |
| 幂等键  | `webhookId` 全局唯一，同一事件的所有重试**复用同一个**                      |
| 顺序   | **不保证有序**。请以 `webhookId`（雪花 ID，单调递增）加资源当前状态判断先后，不要假定到达顺序 |
| 兜底   | 窗口内没收到，用对应的查询接口对账                                        |

<Warning>
  重试窗口只有约 3 分钟。接收服务停机超过这个窗口就会漏事件，**必须用查询接口做兜底对账**，不能只依赖 Webhook。
</Warning>

## 下一步

* 事件全集、信封字段与各事件的 `data` 结构：[事件与数据结构](./events-and-schema)
* 3DS 挑战的接收与回传：[3DS 挑战](../3ds-challenges)
