# Direct Debit webhook payload

> The JSON BCL posts to your webhook URL for Direct Debit sign-ups, mandate approvals and each monthly deduction, field by field.
>
> Source: https://docs.bcl.my/webhook-direct-debit/

A Direct Debit form sends webhooks at three points: when the customer signs up, when the bank approves or rejects the mandate, and for every deduction after that. Each is an HTTP `POST` with a JSON body of `event` and `data`. This page lists every field.

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

## Set your webhook URL

You set the webhook on the Direct Debit form itself:

1. Go to **Payments** > **Forms** and open your Direct Debit form.
2. Click **Advanced** at the top, then open the **Webhook Settings** tab.
3. Turn on **Enable Webhook** and enter one or more **Webhook URLs** (up to 10, each starting with `https://`).
4. Under **Webhook Events**, tick the events you need. See the next section.
5. Click **Save Advanced Settings**.

To try your endpoint, click **Send Test** and choose **Direct Debit - When direct debit transactions occur**. BCL sends three sample requests, one for each stage.

## Which event fires when

The **Direct Debit** checkbox alone does not cover sign-ups. The four events map to the mandate like this:

| Stage | `event` | Tick this event | `record_type` |
| --- | --- | --- | --- |
| Customer submits the form | `form-submit` | **Form Submit** | `DirectDebit` |
| Bank approves the enrolment or a change | `payment-success` | **Payment Success** | `DirectDebit` |
| Bank rejects it, or it fails | `payment-failed` | **Payment Failed** | `DirectDebit` |
| Each deduction | `direct-debit` | **Direct Debit** | `DirectDebitDeduction` |

BCL does not send a webhook when a mandate is terminated.

## Sign-up and approval payload

The `form-submit`, `payment-success` and `payment-failed` events share one shape. This is a `payment-success` for an approved enrolment. The values are made up.

```json
{
  "event": "payment-success",
  "data": {
    "formable_type": "payment_form",
    "formable_id": 34,
    "form_title": "Monthly Fees",
    "form_url": "https://shop.example.com/form/monthly-fees",
    "form_featured_image": "",
    "record_type": "DirectDebit",
    "record_id": "LINK-00041",
    "main_data": {
      "id": "01jfmnrycszgmdhdk78nmtr3gs",
      "payer_name": "Ali bin Abu",
      "payer_email": "ali@example.com",
      "payer_telephone_number": "+60123456789",
      "order_number": "LINK-00041",
      "transaction_type": "live",
      "currency": "MYR",
      "subtotal_amount": "10.00",
      "fee_amount": "1.50",
      "amount": "11.50",
      "application_type": "Enrolment",
      "approval_status": "Approved",
      "bayarcash_mandate_id": "md_AbC123",
      "mandate_reference_number": "E-20261727400000",
      "effective_date": "2026-10-01",
      "frequency_mode": "MT",
      "created_at": "27/09/2026 10:15:00"
    },
    "tracking_params": null,
    "custom_fields_data": {
      "company_name": "Contoh Trading"
    }
  }
}
```

The `data` object has these keys:

| Field | Type | Meaning |
| --- | --- | --- |
| `formable_type` | string | Always `payment_form`. Replaces the old `form_id`. |
| `formable_id` | number | The form's ID. |
| `form_title` | string | Form title. |
| `form_url` | string | Public link to the form, or empty. |
| `form_featured_image` | string | Featured image URL, or empty. |
| `record_type` | string | `DirectDebit`. |
| `record_id` | string | BCL order number of the mandate. It stays the same for every deduction. |
| `main_data` | object | The mandate. See below. |
| `tracking_params` | null | Not recorded for Direct Debit. |
| `custom_fields_data` | object | The form's custom fields, keyed by field name. Absent if there are none. |

There is no `receipt_url` for Direct Debit.

In `main_data`, BCL leaves out fields that are empty:

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string | Internal mandate ID. |
| `payer_name`, `payer_email`, `payer_telephone_number` | string | The customer's details. |
| `order_number` | string | Same as `record_id`. |
| `transaction_type` | string | `live` or `test`. |
| `currency` | string | Usually `MYR`. |
| `subtotal_amount`, `fee_amount` | string | Amount before fees, and the fee. Always sent. |
| `amount` | string | Amount deducted each cycle. |
| `discount_amount`, `coupon_code`, `coupon_usage_id`, `additional_fee_amount`, `tax_amount`, `shipping`, `classification_code` | various | Sent only when the form uses them. Same meaning as on the [payment form payload](/webhook-payment-form/). |
| `application_type` | string | Approval events only: `Enrolment` or `Maintenance` (a change to the mandate). |
| `approval_status` | string | On approval events, one of `New`, `Waiting Approval`, `Failed Bank Verification`, `Active`, `Terminated`, `Approved`, `Rejected`, `Cancelled` or `Error`. On `form-submit` it is the raw status code, or absent. |
| `bayarcash_mandate_id` | string | Bayarcash mandate ID, once the bank has replied. |
| `mandate_reference_number` | string | Mandate reference from the bank. |
| `effective_date`, `expiry_date` | string | Mandate start and end dates. |
| `frequency_mode` | string | Deduction frequency. `MT` is monthly. |
| `created_at` | string | Sign-up time, `DD/MM/YYYY HH:MM:SS`, Malaysia time. |
| `items` | array | Products on the form, if any: `index`, `item`, `option`, `quantity`, `unit_amount`, `amount`, `access_url`, `product_image` and `protected_content`. |

## Deduction payload

BCL sends `direct-debit` for every deduction result Bayarcash reports, successful or not. This payload still uses `form_id`.

```json
{
  "event": "direct-debit",
  "data": {
    "form_id": 34,
    "form_title": "Monthly Fees",
    "record_type": "DirectDebitDeduction",
    "record_id": "LINK-00041",
    "main_data": {
      "order_number": "1-727-400-448-141498",
      "payer_name": "Ali bin Abu",
      "payer_email": "ali@example.com",
      "payer_telephone_number": "+60123456789",
      "mandate_id": "md_AbC123",
      "mandate_reference_number": "E-20261727400000",
      "bayarcash_transaction_id": "trx_3qngpY",
      "batch_number": "HLB1727400000",
      "amount": "11.50",
      "status": 3,
      "status_description": "Successful",
      "datetime": "2026-10-01 00:00:00",
      "cycle": 1
    },
    "custom_fields_data": {
      "company_name": "Contoh Trading"
    }
  }
}
```

The deduction fields mean:

| Field | Type | Meaning |
| --- | --- | --- |
| `form_id` | number | The form's ID. |
| `form_title` | string | Form title. |
| `record_type` | string | `DirectDebitDeduction`. |
| `record_id` | string | BCL order number of the mandate. |
| `main_data.order_number` | string | Reference number of this deduction. |
| `payer_name`, `payer_email`, `payer_telephone_number` | string | The customer's details from the mandate. |
| `mandate_id` | string | Bayarcash mandate ID. |
| `mandate_reference_number` | string | Mandate reference from Bayarcash. |
| `bayarcash_transaction_id` | string | Bayarcash transaction ID for this deduction. |
| `batch_number` | string or null | Bank batch number. |
| `amount` | string | Amount deducted. |
| `status` | number | Deduction status code. `3` means successful. |
| `status_description` | string or null | Status text from Bayarcash. |
| `datetime` | string | Deduction date and time. |
| `cycle` | number or null | Which deduction this is, counting from 1. |
| `custom_fields_data` | object | The form's custom fields. Absent if there are none. |

## Changes from the old sample

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

- Sign-up and approval payloads no longer have `form_id`. Use `formable_id`.
- New fields on sign-up and approval: `formable_type`, `form_url`, `form_featured_image`, `tracking_params`, `subtotal_amount`, `created_at`, and `items` when the form has products.
- Rejected or failed approvals now arrive as `payment-failed`.
- The deduction payload is unchanged.

## Check that a webhook is genuine

BCL does not sign webhooks, so there is no signature to check. Before you act on one, look up the mandate or order through the [BCL API](https://bcl.my/docs/api), use a hard-to-guess URL, and make your handler safe to run twice for the same event. Every attempt is listed under **Tools** > **Webhook Logs**, where you can **Resend** it.

## Common issues

### I ticked Direct Debit but I do not get a webhook when a customer signs up. Why?

The Direct Debit event only covers deductions. The sign-up uses Form Submit, and the bank's approval or rejection uses Payment Success or Payment Failed. Tick all four to follow a mandate from start to finish.

### How do I link a monthly deduction back to the customer's mandate?

In the direct-debit payload, record_id is the BCL order number of the mandate, the same record_id you received at sign-up. main_data.mandate_id is the Bayarcash mandate ID, which matches bayarcash_mandate_id in the approval webhook.

### Why does the test webhook look different from the real one?

The Send Test sample for Direct Debit still uses form_id in the sign-up and approval payloads. Real sign-up and approval webhooks send formable_type and formable_id instead. Deduction webhooks, test and real, use form_id.
