Skip to content

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.

  1. Verify the signature using the steps below.
  2. Check the event fields and save the event to your database or queue before responding.
  3. Return HTTP 200 to 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.

FieldTypeDescription
Content-Typestringapplication/json.
IIMMPACT-SignaturestringContains 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

  1. Read the original body bytes, before JSON parsing. Apply a request-size limit in your server or framework.
  2. Verify the header timestamp and signature using the function below.
  3. If verification fails, return 401. Otherwise, parse and validate the event, save it, then return 200.

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.

EventDescription
payment.pendingWaiting for the customer to complete checkout or authentication.
payment.processingThe payment result is not confirmed yet. Wait before creating another attempt.
payment.cancellation_pendingCancellation was requested, but the result is not confirmed yet.
payment.successfulPayment collection is confirmed. Track the order for fulfillment results.
payment.failedPayment failed. The failure object explains the result.
payment.cancelledCancellation is confirmed. A new payment may be created for the unpaid order.
payment.expiredThe 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.

EventDescription
order.transaction_processingPayment was received and topup/bill fulfillment is in progress.
order.completedEvery 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.

EventDescription
refund.pendingA refund has been created and is waiting to be submitted.
refund.processingThe refund is being processed. Repayment is not yet confirmed.
refund.requires_reviewThe refund was rejected or its result is uncertain. IIMMPACT is reviewing it; do not refund the customer independently.
refund.successfulCustomer repayment is confirmed.

Envelope fields

FieldTypeDescription
event_idstringUnique notification ID. Use it to avoid processing the same event twice.
typestringEvent name; suffix matches data.status.
occurred_atstringRFC3339 event occurrence time.
dataobjectPayment, 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

SituationWhat to do
The same event_id arrives againIf 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 oneKeep 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 paidCall Get Payment Detail.
You need the latest topup results or refund statusCall 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.

IIMMPACT API Documentation