> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chip-in.asia/llms.txt
> Use this file to discover all available pages before exploring further.

# Event Types and Payloads

> Sample payloads for the three CHIP Send webhook events: bank_account_status, send_instruction_status and budget_allocation_status.

CHIP Send delivers three webhook events. Each delivery is a JSON snapshot of the object **after** the change, with the event name in the `Event-Type` header and an `X-Signature` you must verify before parsing — see [Validating Webhooks](https://docs.chip-in.asia/chip-send/api-reference/webhooks/validation.md).

| Event (`Event-Type`) | Fires when |
| - | - |
| `bank_account_status` | A bank account you registered changes status: `received`, `pending`, `verified`, `rejected` or `reviewing` |
| `send_instruction_status` | A send instruction moves through its lifecycle: `received`, `enquiring`, `executing`, `accepted`, `completed`, `rejected`, `reviewing`, or the legacy `deleted` |
| `budget_allocation_status` | A budget **allocation** (`transaction_type: in`) is created, approved or expired. Deductions (`out`) never fire this event — see the note on that section |

Subscribe a webhook to any of them through the `event_hooks` field on [Create a Webhook](https://docs.chip-in.asia/chip-send/api-reference/webhooks/create.md).

## Delivery headers

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `Event-Type` | the event name |
| `X-Signature` | base64-encoded RSA PKCS#1 v1.5 signature of the SHA512 digest of the raw request body |
| `User-Agent` | `CHIP` |

Verify `X-Signature` before trusting a body — see [Validating Webhooks](https://docs.chip-in.asia/chip-send/api-reference/webhooks/validation.md). Failed deliveries are retried; see [Delivery Protocol](https://docs.chip-in.asia/chip-send/api-reference/webhooks/delivery-protocol.md).

> [!WARNING]
>
> Read the raw request body. Framework middleware that parses JSON before your handler runs breaks verification, because re-serialising the parsed object produces different bytes than the ones that were signed. Take the raw buffer, verify, then parse.

## bank\_account\_status

`status` is the status the account just moved into.

| `status` | Meaning |
| - | - |
| `received` | The initial state of a newly registered account, and the state an account is put back into when its verification is restarted after a rejection |
| `pending` | Verification with the bank is in progress |
| `verified` | The bank confirmed the account; `name` is the account holder name the bank returned |
| `rejected` | The bank rejected the account; `rejection_reason` explains why |
| `reviewing` | CHIP is reviewing the account manually |

A webhook is queued when the account record is created, so the first delivery you receive after adding a bank account can already be `pending` or `verified` if verification finished first.

| Field | Type | Notes |
| - | - | - |
| `id` | integer | Bank account id — use this as `bank_account_id` when creating a send instruction |
| `status` | string | see the table above |
| `account_number` | string | as registered |
| `bank_code` | string | BIC of the bank |
| `group_id` | integer or null | your own grouping id, if you set one |
| `name` | string | account holder name |
| `reference` | string or null | your own reference, if you set one |
| `is_debiting_account`, `is_crediting_account` | boolean | role flags, if you set them |
| `deleted_at` | string or null | `null` unless the account was removed |
| `rejection_reason` | string or null | `null` unless `status` is `rejected` |
| `created_at`, `updated_at` | string | UTC, ISO-8601 with milliseconds |

Verified:

```json theme={null}
{
  "id": 1042,
  "status": "verified",
  "account_number": "1234567890",
  "bank_code": "MBBEMYKL",
  "group_id": 17,
  "name": "ACME TRADING SDN BHD",
  "reference": "payroll-main",
  "created_at": "2026-11-11T02:14:07.512Z",
  "is_debiting_account": false,
  "is_crediting_account": false,
  "updated_at": "2026-11-11T02:14:09.884Z",
  "deleted_at": null,
  "rejection_reason": null
}
```

Rejected:

```json theme={null}
{
  "id": 1043,
  "status": "rejected",
  "account_number": "1234567891",
  "bank_code": "MBBEMYKL",
  "group_id": null,
  "name": "ACME TRADING SDN BHD",
  "reference": null,
  "created_at": "2026-11-11T02:30:41.003Z",
  "is_debiting_account": false,
  "is_crediting_account": false,
  "updated_at": "2026-11-11T02:30:44.190Z",
  "deleted_at": null,
  "rejection_reason": "Unable to credit - reject response from beneficiary (may be due to beneficiary account status, i.e. dormant, invalid, closed, blacklisted, on hold or blocked)"
}
```

Pending verification:

```json theme={null}
{
  "id": 1044,
  "status": "pending",
  "account_number": "1234567892",
  "bank_code": "MBBEMYKL",
  "group_id": null,
  "name": "ACME TRADING SDN BHD",
  "reference": null,
  "created_at": "2026-11-11T02:41:12.771Z",
  "is_debiting_account": false,
  "is_crediting_account": false,
  "updated_at": "2026-11-11T02:41:12.771Z",
  "deleted_at": null,
  "rejection_reason": null
}
```

## send\_instruction\_status

`state` is the state the instruction just moved into.

| `state` | Meaning |
| - | - |
| `received` | Created, not yet sent to the bank |
| `enquiring` | Bank account details are being confirmed before the transfer |
| `executing` | The transfer has been submitted to the bank |
| `accepted` | Accepted by the bank but not yet completed (typically a bank cut-off or downtime) |
| `completed` | The money was paid out |
| `rejected` | The transfer failed; `rejection_reason` explains why |
| `reviewing` | CHIP is reviewing the transfer manually |
| `deleted` | Legacy state: the instruction was withdrawn before anything was sent to the bank |

| Field | Type | Notes |
| - | - | - |
| `id` | integer | Send instruction id |
| `bank_account_id` | integer | The bank account the money is paid **to** — the recipient account registered through Add Bank Account, and the same `id` you received on `bank_account_status` |
| `bank_account_details` | object | That recipient account: `name` (account holder), `bank_code` (BIC) and `account_number` |
| `amount` | string | decimal string with two places, for example `"1500.00"` |
| `state` | string | see the table above |
| `rejection_reason` | string or null | `null` unless `state` is `rejected` |
| `email`, `description`, `reference` | string or null | as submitted |
| `slug` | string | receipt slug, always set: 32 hex characters on new instructions, 8 characters on legacy rows |
| `send_recipient_receipt` | boolean | whether a receipt is emailed to the recipient |
| `receipt_url` | string or null | public receipt URL; `null` until the instruction is `completed` |
| `created_at`, `updated_at` | string | UTC, ISO-8601 with milliseconds |

Completed:

```json theme={null}
{
  "id": 88231,
  "bank_account_id": 1042,
  "bank_account_details": {
    "name": "ACME TRADING SDN BHD",
    "bank_code": "MBBEMYKL",
    "account_number": "1234567890"
  },
  "amount": "1500.00",
  "state": "completed",
  "rejection_reason": null,
  "email": "vendor@example.com",
  "description": "Vendor payout for invoice INV-2026-0892",
  "reference": "INV-2026-0892",
  "slug": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "send_recipient_receipt": true,
  "created_at": "2026-11-11T02:20:11.004Z",
  "updated_at": "2026-11-11T02:21:48.771Z",
  "receipt_url": "https://www.chip-in.asia/receipts/send/a1b2c3d4e5f60718293a4b5c6d7e8f90"
}
```

Accepted by the bank (not yet completed):

```json theme={null}
{
  "id": 88232,
  "bank_account_id": 1042,
  "bank_account_details": {
    "name": "ACME TRADING SDN BHD",
    "bank_code": "MBBEMYKL",
    "account_number": "1234567890"
  },
  "amount": "820.00",
  "state": "accepted",
  "rejection_reason": null,
  "email": "vendor@example.com",
  "description": "Vendor payout for invoice INV-2026-0893",
  "reference": "INV-2026-0893",
  "slug": "0f1e2d3c4b5a69788796a5b4c3d2e1f0",
  "send_recipient_receipt": false,
  "created_at": "2026-11-11T02:44:03.118Z",
  "updated_at": "2026-11-11T02:44:19.402Z",
  "receipt_url": null
}
```

Rejected:

```json theme={null}
{
  "id": 88233,
  "bank_account_id": 1042,
  "bank_account_details": {
    "name": "ACME TRADING SDN BHD",
    "bank_code": "MBBEMYKL",
    "account_number": "1234567890"
  },
  "amount": "300.00",
  "state": "rejected",
  "rejection_reason": "Insufficient balance",
  "email": "vendor@example.com",
  "description": "Vendor payout for invoice INV-2026-0894",
  "reference": "INV-2026-0894",
  "slug": "9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a",
  "send_recipient_receipt": false,
  "created_at": "2026-11-11T02:51:22.664Z",
  "updated_at": "2026-11-11T02:51:23.980Z",
  "receipt_url": null
}
```

## budget\_allocation\_status

This event is only delivered for **allocations** — rows with `transaction_type: in`. A row is created when you request a budget allocation, and its `status` moves `pending` → `approved`, or `pending` → `expired`.

> [!NOTE]
>
> Deductions are not delivered here. The ledger also holds `transaction_type: out` rows (the deduction behind a completed send, `status: success`) and you can read them from `GET /send/send_limits`, but they never produce a `budget_allocation_status` webhook. Reconcile completed payouts from `send_instruction_status` instead of waiting for a `success` event here.

| `status` | Meaning |
| - | - |
| `pending` | Created and awaiting the required approvals |
| `approved` | Approved and applied to the available balance |
| `expired` | Not approved within the approval window; `expiry_reason` says why |

| Field | Type | Notes |
| - | - | - |
| `id` | integer | Budget row id |
| `currency` | string | for example `MYR` |
| `fee_type` | string | `flat` on allocations — an allocation carries no fee |
| `transaction_type` | string | always `in` for this event |
| `amount` | number | allocation amount, positive |
| `fee` | number | `0.0` on allocations |
| `net_amount` | number | `amount` minus `fee`, so equal to `amount` on allocations |
| `send_instruction_id` | integer or null | set when the row belongs to a send instruction; `null` for an allocation |
| `bank_account_id` | integer or null | set when the row belongs to a bank account; `null` for an allocation |
| `from_settlement` | string or null | settlement date the allocation applies from (`YYYY-MM-DD`), when set |
| `status` | string | see the table above |
| `approvals_required`, `approvals_received` | integer | number of approvals needed and already given |
| `expiry_reason` | string or null | why the allocation expired; `null` while `pending` or `approved`. One of: `Requested amount exceeds the remaining convertible balance.` · `The 12:00 PM (MYT) approval cut-off for this settlement date has passed.` · `Superseded by a newer budget allocation request.` · `Your settlement is on hold, so budget allocation is unavailable.` |
| `approvals_details` | array | one entry per approver, see below |
| `created_at`, `updated_at` | string | UTC, ISO-8601 with milliseconds |

Each `approvals_details` entry:

| Field | Type | Notes |
| - | - | - |
| `destination_type` | string | delivery channel of the approval request, for example `email` |
| `destination` | string | the approver's destination, for example an email address |
| `status` | string | approval status for that approver |
| `approved_at` | string or null | when that approver approved |
| `expires_at` | string or null | when that approval request lapses |

Pending approval:

```json theme={null}
{
  "id": 4471,
  "currency": "MYR",
  "fee_type": "flat",
  "transaction_type": "in",
  "amount": 2000.0,
  "fee": 0.0,
  "net_amount": 2000.0,
  "send_instruction_id": null,
  "bank_account_id": null,
  "from_settlement": "2026-11-18",
  "status": "pending",
  "approvals_required": 2,
  "approvals_received": 0,
  "expiry_reason": null,
  "approvals_details": [
    {
      "destination_type": "email",
      "destination": "ops@acme.example",
      "status": "pending",
      "approved_at": null,
      "expires_at": "2026-11-12T03:00:00.000Z"
    },
    {
      "destination_type": "email",
      "destination": "finance@acme.example",
      "status": "pending",
      "approved_at": null,
      "expires_at": "2026-11-12T03:00:00.000Z"
    }
  ],
  "created_at": "2026-11-11T03:00:12.400Z",
  "updated_at": "2026-11-11T03:00:12.400Z"
}
```

Approved allocation:

```json theme={null}
{
  "id": 4471,
  "currency": "MYR",
  "fee_type": "flat",
  "transaction_type": "in",
  "amount": 2000.0,
  "fee": 0.0,
  "net_amount": 2000.0,
  "send_instruction_id": null,
  "bank_account_id": null,
  "from_settlement": "2026-11-18",
  "status": "approved",
  "approvals_required": 2,
  "approvals_received": 2,
  "expiry_reason": null,
  "approvals_details": [
    {
      "destination_type": "email",
      "destination": "ops@acme.example",
      "status": "approved",
      "approved_at": "2026-11-11T03:02:11.221Z",
      "expires_at": "2026-11-12T03:00:00.000Z"
    },
    {
      "destination_type": "email",
      "destination": "finance@acme.example",
      "status": "approved",
      "approved_at": "2026-11-11T03:05:44.010Z",
      "expires_at": "2026-11-12T03:00:00.000Z"
    }
  ],
  "created_at": "2026-11-11T03:00:12.400Z",
  "updated_at": "2026-11-11T03:05:44.118Z"
}
```

Expired allocation:

```json theme={null}
{
  "id": 4473,
  "currency": "MYR",
  "fee_type": "flat",
  "transaction_type": "in",
  "amount": 5000.0,
  "fee": 0.0,
  "net_amount": 5000.0,
  "send_instruction_id": null,
  "bank_account_id": null,
  "from_settlement": "2026-11-25",
  "status": "expired",
  "approvals_required": 2,
  "approvals_received": 1,
  "expiry_reason": "The 12:00 PM (MYT) approval cut-off for this settlement date has passed.",
  "approvals_details": [
    {
      "destination_type": "email",
      "destination": "ops@acme.example",
      "status": "approved",
      "approved_at": "2026-11-12T04:10:02.771Z",
      "expires_at": "2026-11-13T04:00:00.000Z"
    },
    {
      "destination_type": "email",
      "destination": "finance@acme.example",
      "status": "expired",
      "approved_at": null,
      "expires_at": "2026-11-13T04:00:00.000Z"
    }
  ],
  "created_at": "2026-11-12T04:00:15.900Z",
  "updated_at": "2026-11-13T04:00:31.204Z"
}
```

## Reading the payloads

* `status` and `state` always describe the object after the change, so an event is a snapshot rather than a diff. Do not infer the previous state from it.
* Intermediate states can be superseded: when an object moves on before an earlier delivery is complete, CHIP suppresses the stale one so you always end at the latest state. Never assume you will receive every step — drive your logic off `state` / `status` in the body.
* Deliveries are retried until your server answers `2xx`, so the same state can arrive more than once, and after a retry it can arrive later than a newer state. Treat each body idempotently on the object `id` plus `state` / `status`.
* Treat the body as the record of that moment. Fetch the object through the API if you need the current state.
* **Live bodies are compact JSON** — one line, no whitespace between fields. The samples on this page are pretty-printed for reading. Verify and parse the raw bytes you received, never a re-serialised copy.
* `rejection_reason` is the human-readable reason; it is `null` for successful states.
* Amounts are not interchangeable across events: send instruction `amount` is a decimal **string** (`"1500.00"`), budget `amount`, `fee` and `net_amount` are **numbers**.
* Timestamps are UTC in ISO-8601 with milliseconds. `from_settlement` is a plain date.
* The ids, names, account numbers and amounts in these examples are illustrative.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.