Appearance
Webhook Setup
Your webhook URL, event subscriptions, and signing secret will be configurable in the IIMMPACT Dashboard.
Receive a webhook
IIMMPACT sends an HTTP POST to your HTTPS endpoint when a payment, order, or refund changes status. The JSON body describes the resource at the time of that event.
- Verify the signature using the steps below.
- Check the event fields and save the event to your database or queue before responding.
- Return HTTP
200to confirm receipt, then process the saved event. Any 2xx response is accepted.
If your endpoint times out or returns a non-2xx response, IIMMPACT retries the notification for up to 72 hours from the first attempt. Each request has a 10-second timeout; retries become less frequent, up to one hour apart. Return 2xx directly rather than redirecting the request.
Signature verification
The signature lets your backend check that the notification came from IIMMPACT and its body was not changed. Use the webhook signing secret, not your API HMAC secret.
| Field | Type | Description |
|---|---|---|
Content-Type | string | application/json. |
IIMMPACT-Signature | string | Contains the delivery timestamp (t) and signature (v1), as shown below. |
http
Content-Type: application/json
IIMMPACT-Signature: t=<Unix-seconds>,v1=<hex-digest>Verification steps
- Read the original body bytes, before JSON parsing. Apply a request-size limit in your server or framework.
- Verify the header timestamp and signature using the function below.
- If verification fails, return
401. Otherwise, parse and validate the event, save it, then return200.
TypeScript example
Works with Bun or Node.js. The function checks the five-minute window and compares HMAC-SHA256 signatures in constant time.
typescript
import { Buffer } from "node:buffer";
import { createHmac, timingSafeEqual } from "node:crypto";
const SIGNATURE_PATTERN = /^t=(\d+),\s*v1=([a-fA-F0-9]{64})$/;
const TIMESTAMP_TOLERANCE_SECONDS = 5 * 60;
export function verifyWebhookSignature(
rawBody: Uint8Array,
signature: string | null,
webhookSecret: string,
nowSeconds = Math.floor(Date.now() / 1000)
): boolean {
const match = SIGNATURE_PATTERN.exec(signature ?? "");
if (!match) return false;
const timestamp = Number(match[1]);
if (
!Number.isSafeInteger(timestamp) ||
!Number.isFinite(nowSeconds) ||
Math.abs(nowSeconds - timestamp) > TIMESTAMP_TOLERANCE_SECONDS
) {
return false;
}
const key = Buffer.from(webhookSecret, "base64");
if (key.length === 0) return false;
const expected = createHmac("sha256", key)
.update(`${match[1]}.`)
.update(rawBody)
.digest();
const received = Buffer.from(match[2], "hex");
return timingSafeEqual(expected, received);
}Pass the exact body bytes, the IIMMPACT-Signature header, and your Base64 webhook secret. The header pattern ensures the received digest has the correct length before comparison. Do not pass JSON.stringify(parsedBody)—even whitespace changes invalidate the signature.
For retried notifications, t is refreshed even if occurred_at is old. Check freshness against t, not occurred_at. During secret rotation, accept either currently active secret; stop accepting a revoked secret immediately.
Event catalog and schemas
Payment events
Each event's data follows the payment response fields, with action omitted. No card input is included.
| Event | Description |
|---|---|
payment.pending | Waiting for the customer to complete checkout or authentication. |
payment.processing | The payment result is not confirmed yet. Wait before creating another attempt. |
payment.cancellation_pending | Cancellation was requested, but the result is not confirmed yet. |
payment.successful | Payment collection is confirmed. Track the order for fulfillment results. |
payment.failed | Payment failed. The failure object explains the result. |
payment.cancelled | Cancellation is confirmed. A new payment may be created for the unpaid order. |
payment.expired | The payment window ended and collection was confirmed unsuccessful. A new payment may be created for the unpaid order. |
Order events
Each event's data follows the order response fields, including per-unit items and refunds known at that time.
| Event | Description |
|---|---|
order.transaction_processing | Payment was received and topup/bill fulfillment is in progress. |
order.completed | Every topup has a final result. Some may have failed; check the transactions. Refunds may still be processing. |
Refund events
Each event's data follows the refund response fields. Track these events separately from order completion.
| Event | Description |
|---|---|
refund.pending | A refund has been created and is waiting to be submitted. |
refund.processing | The refund is being processed. Repayment is not yet confirmed. |
refund.requires_review | The refund was rejected or its result is uncertain. IIMMPACT is reviewing it; do not refund the customer independently. |
refund.successful | Customer repayment is confirmed. |
Envelope fields
| Field | Type | Description |
|---|---|---|
event_id | string | Unique notification ID. Use it to avoid processing the same event twice. |
type | string | Event name; suffix matches data.status. |
occurred_at | string | RFC3339 event occurrence time. |
data | object | Payment, order, or refund details at the time of the event. |
Payment example
Example of a confirmed payment:
json
{
"event_id": "evt_payment_example",
"type": "payment.successful",
"occurred_at": "2026-09-14T10:10:42+08:00",
"data": {
"payment_id": "pay_example",
"order_id": "ord_example",
"external_reference": "ORD-00042",
"customer_external_id": "CUS-1042",
"status": "successful",
"currency": "MYR",
"payment_method_code": "CARD",
"customer_fee_percent": "50.00",
"fee": {
"type": "percentage",
"value": "1.70"
},
"totals": {
"items_subtotal": "100.00",
"fee": {
"total": "1.70",
"customer": "0.85",
"merchant": "0.85"
},
"payment_total": "100.85"
},
"created_at": "2026-09-14T10:05:00+08:00",
"updated_at": "2026-09-14T10:10:42+08:00"
}
}Order example
At fulfillment start, items are still resolving and the refund array may be empty:
json
{
"event_id": "evt_order_example",
"type": "order.transaction_processing",
"occurred_at": "2026-09-14T10:10:42+08:00",
"data": {
"order_id": "ord_example",
"external_reference": "ORD-00042",
"status": "transaction_processing",
"currency": "MYR",
"items_subtotal": "100.00",
"items": [
{
"product": "TNB",
"product_name": "Tenaga Nasional Berhad",
"account": "220012345679",
"amount": "60.00",
"unit_price": "60.00",
"status": "processing",
"status_code": 1,
"refid": "ORD-00042-1",
"cost": "58.1234",
"timestamp": "2026-09-14T10:10:42+08:00"
},
{
"product": "TNB",
"product_name": "Tenaga Nasional Berhad",
"account": "220012345678",
"amount": "40.00",
"unit_price": "40.00",
"status": "processing",
"status_code": 1,
"refid": "ORD-00042-2",
"cost": "39.1250",
"timestamp": "2026-09-14T10:10:42+08:00"
}
],
"refunds": [],
"created_at": "2026-09-14T10:00:00+08:00",
"updated_at": "2026-09-14T10:10:42+08:00"
}
}order.completed contains final per-unit results and the refunds known at that time. See the populated order detail example. Later refund changes appear through refund events or a fresh order read.
Refund example
Example of a refund needing review. A later refund.successful event keeps the same refund ID and sets review_reason to null.
json
{
"event_id": "evt_refund_example",
"type": "refund.requires_review",
"occurred_at": "2026-09-14T10:16:00+08:00",
"data": {
"refund_id": "ref_example",
"order_id": "ord_example",
"payment_id": "pay_example",
"external_reference": "ORD-00042",
"currency": "MYR",
"amount": "40.00",
"reason": "failed_fulfillment",
"status": "requires_review",
"review_reason": "provider_outcome_unknown",
"created_at": "2026-09-14T10:15:20+08:00",
"updated_at": "2026-09-14T10:16:00+08:00"
}
}Duplicate or delayed notifications
| Situation | What to do |
|---|---|
The same event_id arrives again | If you already saved it, return 2xx without processing it again. Retain these IDs for at least seven days. |
| An older update arrives after a newer one | Keep the newer status. For example, a delayed payment.pending must not replace payment.successful. If unsure, read the current payment status. |
| You need to know whether the customer paid | Call Get Payment Detail. |
| You need the latest topup results or refund status | Call Get Order Detail. |
Notifications can arrive late or be missed. A repeated notification does not repeat a charge, topup, or refund. Your handler should also avoid repeating its own actions, such as sending the same receipt twice. Keep signing secrets, PINs, and voucher links out of logs.
