# 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 the moment it happens. When a customer submits a payment form, or their payment succeeds or fails, BCL sends an HTTP `POST` with a JSON body to each URL you set. 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, then open the **Webhook Settings** tab.
3. Turn on **Enable Webhook**.
4. Under **Webhook URLs**, enter your endpoint. Click **Add Webhook URL** 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. Choose an **Event Type**, check the **Webhook URL** and the **Payload Preview**, then click **Send Test Webhook**. Test payloads use random sample values.

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`. Only the ones present are sent. |
| `receipt_url` | string | `payment-success` only. Link to the PDF receipt. Anyone opening it is asked to verify with a one-time code first. |
| `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. A field with no value (null, empty, or `0.00` for most amounts) is left out, so do not rely on every field being present.

| 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 may be 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. Finally, `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 could 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) and confirm the status there.
- 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 quickly and do the heavy work afterwards.

Every attempt is recorded under **Tools** > **Webhook Logs**, with the payload, your response and any error. To send one again, open the row's menu and click **Resend**.

## 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. Pick 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 only Form Submit. 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.

### Will importing old records fire my webhook?

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 it off if you only want the records in BCL.
