# Callback URL和checksum：验证Bayarcash callback

> 设置callback URL（webhook）和return URL。用HMAC-SHA256 checksum签署request并验证callback。
>
> Source: https://docs.bayarcash.com/zh/callbacks-and-checksum/

付款完成时，Bayarcash会用**callback**通知你的服务器，并通过**return URL**把你的顾客送回来。每个callback都带有`checksum`：在信任这些资料之前，先用你的API Secret Key检查它。你也可以在自己的request中加上checksum，让Bayarcash知道它们没有被修改。

## Callback URL和return URL

你在每个payment intent上设置这两个URL（`callback_url`和`return_url`）。它们的作用不同：

| | Callback URL | Return URL |
|---|---|---|
| 由谁调用 | Bayarcash的服务器，直接调用你的服务器 | 你顾客的浏览器，由Bayarcash重定向 |
| 怎样到达 | POST | GET，field在query string中（API v3） |
| 可以用来判断订单状态吗？ | 可以，在检查checksum之后 | 不可以：顾客可能关闭标签页或修改URL |
| 用途 | 把订单标为已付款或失败 | 向顾客显示结果页面 |

只根据callback更新订单。如果return URL先到达，就显示“付款确认中”的信息并等待callback，或用API查询这笔交易。

## Bayarcash发送的callback

每个callback都有一个`record_type` field，告诉你它是什么：

| Callback | 什么时候发送 |
|---|---|
| Pre-transaction | 在付款初期，交易记录还未确定之前。 |
| Transaction | 付款到达最终状态时（成功、失败、取消等）。 |
| FPX Direct Debit authorization | 顾客完成或放弃e-Mandate登记时。 |
| FPX Direct Debit bank approval | 顾客的银行批准或拒绝授权（mandate）时。这可能在几天后才发生。 |
| FPX Direct Debit transaction | 每次从有效的mandate扣款时。 |

交易callback中的`status` field是一个数字：

| 状态 | 意思 |
|---|---|
| 0 | New（新建） |
| 1 | Pending（待处理） |
| 2 | Unsuccessful（不成功） |
| 3 | Successful（成功） |
| 4 | Cancelled（已取消） |
| 5 | Abandoned（已放弃） |

## 每个callback都以HTTP 200回复

你的callback URL在检查并保存结果后，必须回复**HTTP 200**。如果它回复其他状态，或没有回复，Bayarcash会再次发送callback。所以你的handler必须可以安全地运行两次：在完成订单前，先检查你是否已经处理过那个`transaction_id`。

Callback以POST发送。Bayarcash插件和SDK把它们当作表单field读取（PHP中的`$_POST`）。如果你的框架收到的是`application/json`，就改为读取JSON body。

## Checksum怎样运作

签署request和验证callback使用同一个规则。每个调用之间只有field列表不同。要生成checksum：

1. 取出属于该调用的field（见下表），值必须和发送或收到的完全一样。缺少的field用空值。
2. 按field名称从A到Z排序。
3. 按这个顺序把值连接起来，每个值之间加一个`|`。
4. 用你的API Secret Key作为key，计算这段文字的HMAC-SHA256。结果以小写十六进制（hex）表示，就是checksum。

验证callback时，把你的结果和callback带来的`checksum` field比较。使用固定时间比较（constant-time comparison），例如PHP的`hash_equals()`或Python的`hmac.compare_digest()`。如果两者不同，就拒绝这个callback。

request上的checksum是可选的，但建议加上。callback上一定有checksum：一定要验证它。

## 每个checksum中的field

每个调用使用自己的field：

| 调用 | Checksum中的field |
|---|---|
| 创建payment intent（request） | `payment_channel`, `order_number`, `amount`, `payer_name`, `payer_email` |
| FPX Direct Debit登记（request） | `order_number`, `amount`, `payer_name`, `payer_email`, `payer_telephone_number`, `payer_id_type`, `payer_id`, `application_reason`, `frequency_mode` |
| Pre-transaction callback | `record_type`, `exchange_reference_number`, `order_number` |
| Transaction callback | `record_type`, `transaction_id`, `exchange_reference_number`, `exchange_transaction_id`, `order_number`, `currency`, `amount`, `payer_name`, `payer_email`, `payer_bank_name`, `status`, `status_description`, `datetime` |
| Return URL | `transaction_id`, `exchange_reference_number`, `exchange_transaction_id`, `order_number`, `currency`, `amount`, `payer_bank_name`, `status`, `status_description` |
| FPX Direct Debit bank approval callback | `record_type`, `approval_date`, `approval_status`, `mandate_id`, `mandate_reference_number`, `order_number`, `payer_bank_code_hashed`, `payer_bank_code`, `payer_bank_account_no`, `application_type` |
| FPX Direct Debit transaction callback | `record_type`, `batch_number`, `mandate_id`, `mandate_reference_number`, `transaction_id`, `datetime`, `reference_number`, `amount`, `status`, `status_description`, `cycle` |

表格只列出field，顺序由第2步的排序决定。如果一个payment intent有多个`payment_channel`，先用逗号把渠道编号连接起来（`1,5`）再加入，和PHP SDK的做法一样。FPX Direct Debit的修改request和authorization callback，请使用[官方SDK](/zh/sdks/)中的验证方法，它们带有准确的field列表。

## 示例：用PHP验证交易callback

这段代码不使用SDK来检查交易callback。使用[PHP SDK](/zh/php-sdk/)时，`verifyTransactionCallbackData()`一行就能做到同样的事。

```php
function bayarcash_checksum(array $data, array $fields, string $secretKey): string
{
    $payload = [];
    foreach ($fields as $field) {
        $payload[$field] = $data[$field] ?? '';
    }
    ksort($payload);

    return hash_hmac('sha256', implode('|', $payload), $secretKey);
}

$fields = [
    'record_type', 'transaction_id', 'exchange_reference_number',
    'exchange_transaction_id', 'order_number', 'currency', 'amount',
    'payer_name', 'payer_email', 'payer_bank_name', 'status',
    'status_description', 'datetime',
];

$callback = $_POST;
$expected = bayarcash_checksum($callback, $fields, getenv('BAYARCASH_API_SECRET_KEY'));

if (! hash_equals($expected, (string) ($callback['checksum'] ?? ''))) {
    http_response_code(403);
    exit;
}

// Checksum is valid: save the result, then answer 200.
http_response_code(200);
```

## 示例：用Python签署payment intent

同一个规则的Python版本，用于payment intent request：

```python

def bayarcash_checksum(values: dict, secret_key: str) -> str:
    joined = "|".join(str(values[k]) for k in sorted(values))
    return hmac.new(secret_key.encode(), joined.encode(), hashlib.sha256).hexdigest()

checksum = bayarcash_checksum({
    "payment_channel": "1",
    "order_number": "INV-1001",
    "amount": "10.00",
    "payer_name": "Ahmad bin Abdullah",
    "payer_email": "ahmad@example.com",
}, "YOUR_API_SECRET_KEY")
```

把结果作为`checksum`，和其他field一起发送。完整的field参考在[API参考文档的checksum指南](https://api-reference.bayarcash.com/#tag/checksum)。

## 小贴士

- **你的callback URL和return URL要使用HTTPS**。callback带有付款资料，HTTPS能在传输过程中保护这些资料。
- **API Secret Key只能保存在你的服务器上**。任何拿到它的人，都能伪造一个通过checksum检查的callback。
- **如果你的框架有package，就使用它**。Laravel和Django package会加入一个callback route，自动检查checksum，并以HTTP 403拒绝无效的callback。

## 常见问题

### 我的WooCommerce日志显示“Invalid checksum for order”。哪里出错了？

checksum不匹配。最常见的原因是API Secret Key错误，或它来自另一个环境（在正式环境的callback上用了sandbox key，或反过来）。从你正在使用的商家后台重新复制key。

### 即使key正确，我的checksum还是一直不匹配。应该检查什么？

使用该调用的准确field列表，按field名称排序，并完全按照发送或收到的值使用。10.00和10会得出不同的checksum。缺少的field算作空值，不是跳过不算。

### 为什么同一个callback到达了两次？

当你的服务器没有以HTTP 200回复时，Bayarcash会再次发送callback。保存结果后就回复200，并检查交易ID，以免同一笔付款被处理两次。
