# Payment form webhook payload

> The JSON BCL posts to your webhook URL when a customer submits a payment form or a payment succeeds or fails, field by field.
>
> Source: https://docs.bcl.my/webhook-payment-form/

A webhook lets BCL tell your own system about a sale as soon as the sale happens. BCL sends an HTTP `POST` with a JSON body to each URL that you set. BCL sends it when a customer submits a payment form, and when their payment succeeds or fails. This page lists every field in that body.

For the full BCL API, see the [API reference](https://bcl.my/docs/api).

## Set your webhook URL

You set webhooks per form. To turn them on:

1. Go to **Payments** → **Forms** and open your form.
2. Click **Advanced** at the top.

   *(Screenshot: The Advanced tab at the top of the Edit Payment Form page, highlighted)*

3. Open the **Webhook Settings** tab and turn on **Enable Webhook**.

   *(Screenshot: The Webhook Settings tab with Enable Webhook on, one webhook URL and three events ticked)*

4. Under **Webhook URLs**, type your endpoint. If you see no box, click **Add Webhook URL** first. Click **Add Webhook URL** again to add more, up to 10. Every URL must start with `https://`, and every URL receives the same events.
5. Under **Webhook Events**, tick the events you want.
6. Click **Save Advanced Settings**.

To try your endpoint without a real sale, click **Send Test** in the **Webhook Configuration** section. The button shows once the webhook is on and has a URL. Choose an **Event Type**, check the **Webhook URL** and the **Payload Preview**, then click **Send Test Webhook**. Test payloads use random sample values.

*(Screenshot: The Send Test Webhook panel with Payment Success chosen, the payload preview and the Send Test Webhook button highlighted)*

BCL then shows **Test Webhook Sent** with your endpoint's status code, or **Webhook Failed** with the status and the start of its reply. **Webhook Logs** does not list test webhooks.

Event and booking forms have the same **Webhook Settings** tab under **Advanced**, with the same payment payload described here.

## Events

Each request body has two keys: `event` (the event name) and `data` (the payload). These events use the payload on this page:

| Event | When BCL sends it |
| --- | --- |
| `form-submit` | The customer submits the form, before payment. |
| `payment-success` | Bayarcash confirms the payment. |
| `payment-failed` | The payment fails or is cancelled. |

For Direct Debit forms, see [Direct Debit webhook payload](/webhook-direct-debit/). Event forms can also send `ticket-checked-in`, and booking forms `booking-confirmed` and `booking-cancelled`. Those use a different payload.

## Sample payload

This is a `payment-success` request. The values are made up.

```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"
    }
  }
}
```

## Top-level fields

The `data` object contains these keys:

| Field | Type | Meaning |
| --- | --- | --- |
| `formable_type` | string | Form type: `payment_form`, `event_form` or `booking_form`. |
| `formable_id` | number | The form's ID. Replaces the old `form_id`. |
| `form_title` | string | Form title. For event forms, the event name. |
| `form_url` | string | Public link to the form. Empty if the form has no slug or domain. |
| `form_featured_image` | string | Featured image URL, or empty. |
| `record_type` | string | Always `Transaction` on this page. |
| `record_id` | string | The BCL order number, for example `LINK-02046`. Use it to match records. |
| `main_data` | object | The order. See the next section. |
| `tracking_params` | object or null | UTM and click IDs captured when the customer opened the form: `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `utm_id`, `fbclid`, `ttclid`, `gclid`. BCL sends only the ones that are present. |
| `receipt_url` | string | `payment-success` only. Link to the PDF receipt. Anyone who opens it must first verify with a one-time code. |
| `custom_fields_data` | object | The form's custom fields and address fields, keyed by field name. Absent if there are none. |
| `affiliate_data` | object | Present only when an affiliate referred the sale. |

## Order fields (main_data)

BCL sends the order record and drops empty values. BCL leaves out a field with no value (null, empty, or `0.00` for most amounts). So do not expect every field in every request.

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string | Internal transaction ID. |
| `payer_name`, `payer_email`, `payer_telephone_number` | string | The buyer's details. |
| `order_number` | string | Same as `record_id`. |
| `transaction_type` | string | `live` or `test`. |
| `currency` | string | Usually `MYR`. |
| `subtotal_amount` | string | Item total before fees. Always sent. |
| `discount_amount` | string | Coupon discount. |
| `coupon_code`, `coupon_usage_id` | string, number | The coupon used. |
| `fee_amount` | string | Fee charged to the buyer. Always sent. |
| `additional_fee_amount` | string | Extra fee set on the form. |
| `rounding_adjustment` | string | Rounding applied to the total. |
| `tax_amount` | object | Tax details, such as the tax name, rate and amount. |
| `shipping` | object | Shipping method `name`, `cost` and `total_weight`. |
| `bank_transfer_details` | object | Bank details, for manual transfer orders. |
| `amount` | string | Total the buyer pays. |
| `payment_channel` | string | Channel name, for example `FPX`, `DuitNow QR`, `Credit Card`, `Cash on Delivery (COD)` or `Manual Bank Transfer`. |
| `status` | number | Payment status code. `3` means successful and `2` means failed. It is sometimes absent on `form-submit`. |
| `status_description` | string | Status text from Bayarcash. `payment-success` and `payment-failed` only. |
| `is_paid` | number | `1` once paid, otherwise `0`. |
| `retry_count` | number | How many times the buyer retried payment. |
| `created_at` | string | Order time, `DD/MM/YYYY HH:MM:SS`, Malaysia time. |
| `customer_id` | string | BCL customer ID. |
| `classification_code` | string | e-invoice classification code. |
| `affiliate_id`, `cookie_id` | string | Affiliate attribution, when present. |
| `source_type` | string | Same as `formable_type`. |
| `items` | array | Items bought. See the next section. |

## Item fields

Each entry in `items` has these keys:

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | number | Line item ID. |
| `index`, `productable_id` | number | Product ID. `index` is kept for older integrations. |
| `productable_type` | string | `product_item`, `event_ticket` or `booking_service`, for example. |
| `item`, `sku`, `option` | string | Product name, SKU and chosen variation. |
| `quantity` | number | Quantity. |
| `unit_amount`, `normal_price`, `sale_price` | string | Price paid per unit, normal price and sale price. |
| `amount` | string | Line total. |
| `access_url` | string or null | Download link for a digital product. |
| `weight`, `total_weight` | string | Weight per unit and for the line. |
| `metadata` | object or null | Extra item data. |
| `product_image` | string | Product image URL, or empty. |
| `protected_content` | array | `payment-success` only: each item has `title`, `url` (the buyer's personal access link) and `access_finder_url` (the page where a buyer who lost the link can find it again). Empty on other events. |

## Affiliate fields

The `affiliate_data` object has `referral_id`, `affiliate_id`, `affiliate_username`, `affiliate_name`, `affiliate_email` and `affiliate_phone`. It also has `commission_type` (`flat`, `percentage` or `per_item`), `commission_rate` and `commission_amount`. For per-item commission, `commission_rate` is null and `commission_breakdown` lists each item. Last, it has `status` (for example `pending` or `approved`) and `referral_source`.

## Changes from the old sample

If you built your integration from the old sample, check these changes:

- `form_id` is gone. Use `formable_id`, and `formable_type` for the form type.
- New top-level fields: `formable_type`, `form_url`, `form_featured_image` and `tracking_params`.
- New order fields: `subtotal_amount`, `created_at`, `customer_id`, `is_paid`, `retry_count`, `source_type` and the discount, coupon, tax and shipping fields.
- New item fields: `id`, `productable_type`, `productable_id`, `sku`, `normal_price`, `sale_price`, `weight`, `total_weight`, `metadata`, `product_image` and `protected_content`.
- New affiliate field: `commission_breakdown`.
- New event: `payment-failed`.

## Check that a webhook is genuine

BCL does not sign webhooks. There is no signature header or shared secret, so anyone who knows your URL can post to it. To stay safe:

- Before you ship an order or grant access, look up the order by `record_id` through the [BCL API](https://bcl.my/docs/api). Confirm the status there. In Test Mode, use `GET /transaction/{order_number}`: `GET /transactions/{order_number}` only finds live payments. Send your own `User-Agent` header, because BCL blocks the default ones from tools such as curl and python-requests.
- Use a URL that is hard to guess, for example with a long random token in the path.
- Handle the same event more than once safely. Use `record_id` and `event` together to spot repeats.

## Delivery and logs

BCL sends each event once to each URL, one request per URL. Any 2xx response counts as delivered. Reply at once, and do the heavy work after you reply.

BCL records every attempt under **Tools** → **Webhook Logs**, with the payload, your response and any error. BCL does not retry a failed delivery on its own. To send one again:

1. Go to **Tools** → **Webhook Logs**.
2. Click the menu at the end of the row, then click **Resend** and confirm.

   *(Screenshot: The Webhook Logs list with a row menu open and Resend highlighted)*

Click **View Details** in the same menu to see the payload BCL sent and your endpoint's reply.

## Common issues

### I added my webhook URL but my system receives nothing. How do I test it?

Open the form's Webhook Settings tab and click Send Test. Choose an Event Type, check the Webhook URL and send. If the test arrives but real payments do not, check that you ticked Payment Success, and not Form Submit alone. If the test does not arrive, make sure your URL is public, starts with https://, accepts POST and replies with a 2xx status.

### How can my own system know when a payment comes in?

Turn on the webhook for the form and tick Payment Success. BCL posts the order, buyer, items and custom fields to your URL as soon as the payment is confirmed. Match it to your records with record_id, which is the BCL order number.

### Does my webhook fire when I import old records?

Only if you tick Run webhooks and automations on the import page. Each imported row then triggers your webhooks as if it were a real payment. So leave the box unticked if you only want the records in BCL.
