# WEBHOOK 说明

> 了解发卡事件推送的报文头、报文结构、商户响应要求和 HMAC-SHA256 签名验签方式。

接入发卡事件推送前，请先阅读本页公共规则。Onerway 会将卡操作、卡交易和 3DS 事件推送到商户配置的回调地址。

## 报文头

| Header | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `Content-Type` | String | 是 | 固定为 `application/json;charset=UTF-8`。 |
| `x-timestamp` | String | 是 | Unix 秒级时间戳。 |
| `x-signature` | String | 是 | 使用 `webhook_secret`、`x-timestamp + "." + raw_body` 生成 HMAC-SHA256 签名，参考[验签与签名](/zh/issuing/api-reference/webhook-description#%E9%AA%8C%E7%AD%BE%E4%B8%8E%E7%AD%BE%E5%90%8D)。 |

## 报文结构

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `request_id` | String | 是 | 唯一事件标识，用于幂等去重。 |
| `event_type` | String | 是 | 事件类型。发卡当前推送 `issuing.cardOperateEvent`、`issuing.cardTransactionEvent` 和 `issuing.card3dsEvent`。 |
| `created_at` | String | 是 | 事件创建时间，ISO 8601 格式。 |
| `version` | String | 否 | 创建 Webhook 订阅时选择的 API 版本；未指定时默认为 `1.0`。 |
| `data` | Object | 是 | 业务数据，结构随 `event_type` 变化。 |

## 商户响应要求

商户回调接口必须返回 JSON 响应。Onerway 根据该响应判断推送是否成功。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `respCode` | String | 返回 `20000` 表示确认收到。 |
| `respMsg` | String | 响应信息。 |

如果回调响应码不是 `20000`，Onerway 会每 15 秒重试一次，最多重试 10 次。

## 验签与签名

Onerway 会对卡操作、卡交易和 3DS 等异步通知进行签名。商户处理事件前应先完成验签。

签名原文按以下方式拼接：

```text
x-timestamp + "." + raw_body
```

使用 HMAC-SHA256 生成期望签名，并与 `x-signature` 进行大小写不敏感比较。

<code-collapse name="签名示例">
<code-group sync="issuing-webhook-signing-lang">

```java [Java]
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HexFormat;

public final class HmacSHA256Util {
    private HmacSHA256Util() {
    }

    public static String signHmacSHA256(String webhook_secret, String content) throws Exception {
        byte[] key = Base64.getDecoder().decode(webhook_secret);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return HexFormat.of().formatHex(mac.doFinal(content.getBytes(StandardCharsets.UTF_8)));
    }

    public static boolean verifyHmacSHA256(String webhook_secret, String content, String signed) throws Exception {
        String expected = signHmacSHA256(webhook_secret, content);
        return MessageDigest.isEqual(
            expected.toLowerCase().getBytes(StandardCharsets.UTF_8),
            signed.toLowerCase().getBytes(StandardCharsets.UTF_8)
        );
    }
}
```

```python [Python]
import base64
import hashlib
import hmac

def sign_hmac_sha256(webhook_secret: str, content: str) -> str:
    key = base64.b64decode(webhook_secret)
    return hmac.new(key, content.encode("utf-8"), hashlib.sha256).hexdigest()

def verify_hmac_sha256(webhook_secret: str, content: str, signed: str) -> bool:
    expected = sign_hmac_sha256(webhook_secret, content)
    return hmac.compare_digest(expected.lower(), signed.lower())
```

```php [PHP]
<?php

final class HmacSHA256Util
{
    public static function signHmacSHA256(string $webhook_secret, string $content): string
    {
        $key = base64_decode($webhook_secret, true);
        if ($key === false) {
            throw new InvalidArgumentException('Invalid webhook secret.');
        }

        return hash_hmac('sha256', $content, $key);
    }

    public static function verifyHmacSHA256(string $webhook_secret, string $content, string $signed): bool
    {
        $expected = self::signHmacSHA256($webhook_secret, $content);
        return hash_equals(strtolower($expected), strtolower($signed));
    }
}
```

</code-group>
</code-collapse>
