# 付款表单webhook payload

> 顾客提交付款表单、付款成功或失败时，BCL发送到你webhook URL的JSON，逐个栏位说明。
>
> Source: https://docs.bcl.my/zh/webhook-payment-form/

Webhook让BCL在销售发生的那一刻通知你自己的系统。当顾客提交付款表单，或付款成功或失败时，BCL会向你设置的每个URL发送一个HTTP `POST`，body是JSON。本页列出这个body里的每个栏位。

完整的BCL API，请看[API参考文档](https://bcl.my/docs/api)。

## 设置你的webhook URL

Webhook是按表单设置的。开启方法：

1. 前往**Payments** → **Forms**，打开你的表单。
2. 点击顶部的**Advanced**。

   *(Screenshot: Edit Payment Form页面顶部的Advanced标签页已标示)*

3. 打开**Webhook Settings**标签页，开启**Enable Webhook**。

   *(Screenshot: Webhook Settings标签页，Enable Webhook已开启，有一个webhook URL，勾选了三个事件)*

4. 在**Webhook URLs**下输入你的endpoint。如果没有显示输入框，先点击**Add Webhook URL**。再点击**Add Webhook URL**可以添加更多，最多10个。每个URL都必须以`https://`开头，而且每个URL都会收到相同的事件。
5. 在**Webhook Events**下，勾选你要的事件。
6. 点击**Save Advanced Settings**。

要在没有真实销售的情况下测试你的endpoint，就在**Webhook Configuration**部分点击**Send Test**。Webhook开启并有URL后，这个按钮才会出现。选择**Event Type**，检查**Webhook URL**和**Payload Preview**，然后点击**Send Test Webhook**。测试payload使用随机的范例值。

*(Screenshot: Send Test Webhook面板，已选择Payment Success，显示payload预览，Send Test Webhook按钮已标示)*

之后BCL会显示**Test Webhook Sent**和你endpoint的状态码，或显示**Webhook Failed**、状态码和回复的开头部分。测试webhook不会列在**Webhook Logs**里。

活动表单和预约表单的**Advanced**下也有同样的**Webhook Settings**标签页，使用本页说明的相同付款payload。

## 事件

每个请求的body有两个键：`event`（事件名称）和`data`（payload）。以下事件使用本页的payload：

| 事件 | BCL什么时候发送 |
| --- | --- |
| `form-submit` | 顾客在付款前提交表单。 |
| `payment-success` | Bayarcash确认付款。 |
| `payment-failed` | 付款失败或被取消。 |

Direct Debit表单请看[直接扣账（Direct Debit）webhook payload](/zh/webhook-direct-debit/)。活动表单也可以发送`ticket-checked-in`，预约表单可以发送`booking-confirmed`和`booking-cancelled`。这些事件使用不同的payload。

## 范例payload

这是一个`payment-success`请求。数值都是虚构的。

```json
{
  "event": "payment-success",
  "data": {
    "formable_type": "payment_form",
    "formable_id": 210,
    "form_title": "Online Cooking Class",
    "form_url": "https://shop.example.com/form/online-cooking-class",
    "form_featured_image": "https://cdn.example.com/banner.jpg",
    "record_type": "Transaction",
    "record_id": "LINK-02046",
    "main_data": {
      "id": "01jfps609t08vjb8d5exq27bz6",
      "payer_name": "Ali bin Abu",
      "payer_email": "ali@example.com",
      "payer_telephone_number": "+60123456789",
      "order_number": "LINK-02046",
      "transaction_type": "live",
      "currency": "MYR",
      "subtotal_amount": "100.00",
      "fee_amount": "1.00",
      "amount": "101.00",
      "payment_channel": "FPX",
      "status": 3,
      "status_description": "Approved",
      "created_at": "27/09/2026 14:05:09",
      "customer_id": "01jfps5zq1m2k8c4x7v9b3n6td",
      "is_paid": 1,
      "retry_count": 0,
      "source_type": "payment_form",
      "items": [
        {
          "id": 5501,
          "index": 101,
          "productable_type": "product_item",
          "productable_id": 101,
          "item": "Cooking Class Recording",
          "sku": "CC-001",
          "option": "",
          "quantity": 1,
          "unit_amount": "100.00",
          "normal_price": "120.00",
          "sale_price": "100.00",
          "amount": "100.00",
          "access_url": null,
          "weight": "0.00",
          "total_weight": "0.00",
          "metadata": null,
          "product_image": "https://cdn.example.com/class.jpg",
          "protected_content": [
            {
              "title": "Module 1",
              "url": "https://shop.example.com/content/abc123token",
              "access_finder_url": "https://shop.example.com/content/7/access"
            }
          ]
        }
      ]
    },
    "tracking_params": {
      "utm_source": "facebook",
      "utm_campaign": "september-promo"
    },
    "receipt_url": "https://bcl.my/storage/pdf/LINK-02046.pdf",
    "custom_fields_data": {
      "address_line1": "1 Jalan Contoh",
      "city": "Kajang",
      "state": "Selangor",
      "postal_code": "43000",
      "country": "MY"
    },
    "affiliate_data": {
      "referral_id": 57,
      "affiliate_id": 12,
      "affiliate_username": "siti",
      "affiliate_name": "Siti binti Ahmad",
      "affiliate_email": "siti@example.com",
      "affiliate_phone": "+60198765432",
      "commission_type": "percentage",
      "commission_rate": "10.00",
      "commission_amount": "10.00",
      "commission_breakdown": null,
      "status": "approved",
      "referral_source": "direct"
    }
  }
}
```

## 顶层栏位

`data`对象包含这些键：

| 栏位 | 类型 | 意思 |
| --- | --- | --- |
| `formable_type` | string | 表单类型：`payment_form`、`event_form`或`booking_form`。 |
| `formable_id` | number | 表单ID。取代旧的`form_id`。 |
| `form_title` | string | 表单标题。活动表单则是活动名称。 |
| `form_url` | string | 表单的公开链接。如果表单没有slug或域名，就是空的。 |
| `form_featured_image` | string | 封面图片URL，或空值。 |
| `record_type` | string | 在本页永远是`Transaction`。 |
| `record_id` | string | BCL订单号码，例如`LINK-02046`。用它来对应记录。 |
| `main_data` | object | 订单。请看下一节。 |
| `tracking_params` | object或null | 顾客打开表单时记录的UTM和点击ID：`utm_source`、`utm_medium`、`utm_campaign`、`utm_content`、`utm_term`、`utm_id`、`fbclid`、`ttclid`、`gclid`。只发送有值的项目。 |
| `receipt_url` | string | 仅限`payment-success`。PDF收据的链接。任何人打开它，都要先用一次性验证码验证。 |
| `custom_fields_data` | object | 表单的自定义栏位和地址栏位，以栏位名称为键。如果没有，就不会出现。 |
| `affiliate_data` | object | 只有在affiliate推荐这笔销售时才会出现。 |

## 订单栏位（main_data）

BCL发送订单记录时会去掉空值。没有值的栏位（null、空的，或大部分金额为`0.00`）不会发送，所以不要以为每个栏位都一定存在。

| 栏位 | 类型 | 意思 |
| --- | --- | --- |
| `id` | string | 内部交易ID。 |
| `payer_name`, `payer_email`, `payer_telephone_number` | string | 买家的资料。 |
| `order_number` | string | 与`record_id`相同。 |
| `transaction_type` | string | `live`或`test`。 |
| `currency` | string | 通常是`MYR`。 |
| `subtotal_amount` | string | 未加手续费的产品总额。一定会发送。 |
| `discount_amount` | string | 优惠券折扣。 |
| `coupon_code`, `coupon_usage_id` | string, number | 使用的优惠券。 |
| `fee_amount` | string | 向买家收取的手续费。一定会发送。 |
| `additional_fee_amount` | string | 表单上设置的额外费用。 |
| `rounding_adjustment` | string | 总额的四舍五入调整。 |
| `tax_amount` | object | 税务详情，例如税名、税率和金额。 |
| `shipping` | object | 运送方式的`name`、`cost`和`total_weight`。 |
| `bank_transfer_details` | object | 银行资料，用于手动转账的订单。 |
| `amount` | string | 买家支付的总额。 |
| `payment_channel` | string | 渠道名称，例如`FPX`、`DuitNow QR`、`Credit Card`、`Cash on Delivery (COD)`或`Manual Bank Transfer`。 |
| `status` | number | 付款状态码。`3`表示成功，`2`表示失败。在`form-submit`可能不会出现。 |
| `status_description` | string | 来自Bayarcash的状态文字。仅限`payment-success`和`payment-failed`。 |
| `is_paid` | number | 已付款为`1`，否则为`0`。 |
| `retry_count` | number | 买家重试付款的次数。 |
| `created_at` | string | 下单时间，`DD/MM/YYYY HH:MM:SS`，马来西亚时间。 |
| `customer_id` | string | BCL顾客ID。 |
| `classification_code` | string | 电子发票（e-Invoice）分类代码。 |
| `affiliate_id`, `cookie_id` | string | Affiliate归属，有的话才会出现。 |
| `source_type` | string | 与`formable_type`相同。 |
| `items` | array | 购买的产品。请看下一节。 |

## 产品栏位

`items`里的每一项都有这些键：

| 栏位 | 类型 | 意思 |
| --- | --- | --- |
| `id` | number | 订单明细ID。 |
| `index`, `productable_id` | number | 产品ID。保留`index`是为了旧的整合。 |
| `productable_type` | string | 例如`product_item`、`event_ticket`或`booking_service`。 |
| `item`, `sku`, `option` | string | 产品名称、SKU和所选的规格。 |
| `quantity` | number | 数量。 |
| `unit_amount`, `normal_price`, `sale_price` | string | 每件实付价格、原价和特价。 |
| `amount` | string | 该行总额。 |
| `access_url` | string或null | 数码产品的下载链接。 |
| `weight`, `total_weight` | string | 每件重量和该行总重量。 |
| `metadata` | object或null | 额外的产品数据。 |
| `product_image` | string | 产品图片URL，或空值。 |
| `protected_content` | array | 仅限`payment-success`：每一项有`title`、`url`（买家的个人访问链接）和`access_finder_url`（弄丢链接的买家可以重新找回链接的页面）。其他事件则是空的。 |

## Affiliate栏位

`affiliate_data`对象有`referral_id`、`affiliate_id`、`affiliate_username`、`affiliate_name`、`affiliate_email`和`affiliate_phone`。它也有`commission_type`（`flat`、`percentage`或`per_item`）、`commission_rate`和`commission_amount`。按产品佣金时，`commission_rate`是null，`commission_breakdown`会列出每个产品。最后是`status`（例如`pending`或`approved`）和`referral_source`。

## 与旧范例的不同

如果你是根据旧范例创建整合的，请检查这些变更：

- `form_id`已取消。使用`formable_id`，表单类型则用`formable_type`。
- 新的顶层栏位：`formable_type`、`form_url`、`form_featured_image`和`tracking_params`。
- 新的订单栏位：`subtotal_amount`、`created_at`、`customer_id`、`is_paid`、`retry_count`、`source_type`，以及折扣、优惠券、税和运送栏位。
- 新的产品栏位：`id`、`productable_type`、`productable_id`、`sku`、`normal_price`、`sale_price`、`weight`、`total_weight`、`metadata`、`product_image`和`protected_content`。
- 新的affiliate栏位：`commission_breakdown`。
- 新事件：`payment-failed`。

## 确认webhook是真的

BCL不会为webhook签名。没有signature header，也没有共享的secret，所以任何知道你URL的人都可以向它发送请求。为了安全：

- 在你寄出订单或给予访问权限之前，先通过[BCL API](https://bcl.my/docs/api)用`record_id`查找订单，并在那里确认状态。在测试模式下，使用`GET /transaction/{order_number}`：`GET /transactions/{order_number}`只能找到真实付款。发送你自己的`User-Agent` header，因为BCL会封锁curl和python-requests等工具的默认User-Agent。
- 使用难以猜到的URL，例如在路径里加入一段很长的随机token。
- 确保同一个事件处理多次也不会出问题。同时用`record_id`和`event`找出重复的请求。

## 发送和记录

BCL会把每个事件向每个URL发送一次，每个URL一个请求。任何2xx回复都算发送成功。请尽快回复，繁重的工作之后再做。

每次尝试都记录在**Tools** → **Webhook Logs**，包括payload、你的回复和任何错误。BCL不会自动重试失败的发送。要重新发送：

1. 前往**Tools** → **Webhook Logs**。
2. 点击该行末端的菜单，然后点击**Resend**并确认。

   *(Screenshot: Webhook Logs列表，已打开某一行的菜单，Resend已标示)*

在同一个菜单点击**View Details**，查看BCL发送的payload和你endpoint的回复。

## 常见问题

### 我已添加webhook URL，但我的系统什么都没收到。怎样测试？

打开表单的Webhook Settings标签页，点击Send Test。选择Event Type，检查Webhook URL，然后发送。如果测试收到了，真实付款却没有，检查你是否勾选了Payment Success，而不是只勾选Form Submit。如果测试没有收到，确保你的URL是公开的、以https://开头、接受POST，并回复2xx状态。

### 我自己的系统怎样知道有付款进来？

为表单开启webhook，并勾选Payment Success。付款一确认，BCL就会把订单、买家、产品和自定义栏位发送到你的URL。用record_id（即BCL订单号码）对应你的记录。

### 导入旧记录会触发我的webhook吗？

只有在导入页面勾选Run webhooks and automations时才会。这样每一行导入的记录都会像真实付款一样触发你的webhook，所以如果你只想把记录放进BCL，就不要勾选。
