Skip to content

Catalog API

The Catalog API returns the complete product catalog with hierarchical structure and field configurations. This endpoint is designed for device caching — fetch once, render dynamically.

Migrating from /v2/product-list?

This endpoint replaces the legacy Product Listing API. See the Migration Guide for endpoint mapping, field changes, and a migration checklist.

Endpoint

http
GET https://api.iimmpact.com/v2/catalog

Request Headers

HeaderDescriptionRequired
X-Api-KeyYour API keyYes
X-TimestampCurrent Unix timestamp in secondsYes
X-NonceUnique nonce for replay protectionYes
X-SignatureHMAC signature in format v1=<base64>Yes

See API Key Authentication for canonical-string rules and signature generation.

Query Parameters

ParameterTypeRequiredDescription
product_codestringNoReturn only one product in products map
is_activebooleanNoFilter by catalog active status first, then reseller override status (true/false)
include_hiddenbooleanNoInclude hidden products. Default is false

Response Headers

This endpoint currently does not guarantee conditional-cache headers (Last-Modified / ETag). Use the response body last_updated field for cache freshness checks.

Response Structure

json
{
  "last_updated": "2025-01-07T00:00:00Z",

  "tree": {
    "groups": [...]
  },

  "products": {
    "D": {...},
    "M": {...}
  }
}
FieldTypeDescription
last_updatedstringISO 8601 timestamp of last catalog update
treeobjectHierarchical structure for UI navigation
productsobjectFlat map of products keyed by product_code

Tree Structure

The tree provides hierarchical navigation (group → category → products):

json
{
  "tree": {
    "groups": [
      {
        "id": "grp_mobile",
        "name": "Mobile",
        "icon_url": "https://cdn.iimmpact.com/icons/mobile.png",
        "categories": [
          {
            "id": "cat_prepaid",
            "name": "Prepaid Reload",
            "product_codes": ["D", "M", "C", "U"]
          },
          {
            "id": "cat_postpaid",
            "name": "Postpaid Bills",
            "product_codes": ["DB", "MB", "CB"]
          }
        ]
      },
      {
        "id": "grp_bills",
        "name": "Bills",
        "icon_url": "https://cdn.iimmpact.com/icons/bills.png",
        "categories": [
          {
            "id": "cat_utilities",
            "name": "Utilities",
            "product_codes": ["TNB", "IW", "AKSB"]
          }
        ]
      }
    ]
  }
}

Why Separate Tree and Products?

  • tree: Controls UI hierarchy and ordering
  • products: Flat map for O(1) lookup and efficient local storage

Product Schema

Each product in the products map contains field configurations and fulfillment mapping:

json
{
  "D": {
    "code": "D",
    "name": "Digi Prepaid",
    "note": "Prepaid reload for Digi mobile numbers",
    "image_url": "https://dashboard.iimmpact.com/img/D.png",
    "processing_time": "instant",
    "is_active": true,
    "denomination": "5,10,30,50,100",

    "fields": [...],
    "fulfillment": {...},
    "pricing": {
      "cost": {
        "model": "percentage_discount",
        "percentage_rate": 0.985
      },
      "price_adjustment": null,
      "has_loss_risk": false
    }
  }
}
FieldTypeDescription
codestringUnique product identifier
namestringDisplay name
notestring | nullOptional extra description for the product
image_urlstringProduct logo URL
processing_timestringinstant, 24_hours, or 3_days
is_activebooleanEffective product status after reseller overlay
denominationstring | nullRaw denomination source string from backend
fieldsarrayForm field definitions
fulfillmentobjectMapping to payment request
pricingobjectTenant pricing metadata (cost, optional price_adjustment, has_loss_risk)
min_amountobject | nullOptional custom minimum amount override for pricing fields
max_amountobject | nullOptional custom maximum amount override for pricing fields

Processing Time Values

The processing_time field indicates how long before the transaction is fulfilled:

ValueDescriptionUser MessageExamples
instantProcessed immediately"Delivered within seconds"Mobile reload, games
24_hoursProcessed within 1 business day"Processing within 24 hours"PTPTN, some utilities
3_daysProcessed within 3 business days"Processing within 3 business days"Insurance, special bills

User Experience

Display the processing_time to users before checkout so they understand when to expect fulfillment.


Field Schema

Fields are the building blocks of the product form. We use primitive types with optional input_mode hints for UX:

json
{
  "id": "phone",
  "type": "text",
  "input_mode": "tel",
  "label": "Phone Number",
  "placeholder": "e.g. 0123456789",
  "required": true,
  "order": 1,
  "role": "account",
  "validation": {
    "pattern": "^01[0-9]{8,9}$",
    "message": "Enter valid Malaysian phone number"
  }
}

Field Properties

PropertyTypeRequiredDescription
idstringYesUnique identifier within product
typestringYestext, number, select, money
input_modestringNoKeyboard/input hint (see table below)
labelstringYesDisplay label
placeholderstringNoInput placeholder text
requiredbooleanYesWhether field is required
ordernumberNoDisplay order (ascending)
rolestringNoaccount, pricing, or none
validationobjectNoValidation rules
data_sourceobjectNoFor select fields only
currencystringNoFor money fields only (default: MYR)

Field Types

TypeDescriptionUse Case
textFree-form text inputPhone, NRIC, account numbers
numberNumeric inputPlayer IDs, quantities
selectDropdown from optionsPlans, packages, billers
moneyCurrency amount with decimalsPayment amounts

Input Modes

The input_mode property hints which keyboard/input method to use. It does not affect validation — use the validation object for that.

Input ModeKeyboard TypeUse Case
textDefault keyboardGeneral text (default)
telPhone dialpadPhone numbers
numericNumber padNRIC, account numbers
emailEmail keyboardEmail addresses
decimalNumber + decimalAmounts (for money type)

Validation Object

json
{
  "validation": {
    "pattern": "^[0-9]{12}$",
    "message": "Enter valid 12-digit NRIC",
    "min": 10,
    "max": 60000
  }
}
PropertyTypeDescription
patternstringRegex pattern for text validation
messagestringError message when validation fails
minnumberMinimum value (for money/number)
maxnumberMaximum value

Data Source (Select Fields)

json
{
  "data_source": {
    "type": "dynamic",
    "depends_on": ["phone"],
    "endpoint": "/options",
    "params": {
      "product_code": { "static": "HI" },
      "field_id": { "static": "plan" },
      "account_number": { "from_field": "phone" }
    }
  }
}
PropertyTypeDescription
typestringreference (static) or dynamic (user-specific)
depends_onarrayField IDs that must be filled first
endpointstringAPI endpoint to fetch options
paramsobjectQuery parameters with static or dynamic values

Fulfillment Schema

Declares how fields map to the /v2/topup payment request.

The fulfillment block covers account, amount, and extras. The product field is always the product's code. You still need to provide your own refid (idempotency) and optional remarks when calling /v2/topup.

Amount = Price

The amount in the payment request is the selected transaction amount (price.amount from options, or direct money input). Pricing metadata (cost / price_adjustment) is for your backend calculations.

json
{
  "fulfillment": {
    "account": { "from_field": "phone" },
    "amount": { "from_field": "plan", "path": "price.amount" },
    "extras": {
      "subproduct_code": { "from_field": "plan", "path": "code" },
      "ic_number": { "from_field": "nric" },
      "ref2": { "from_field": "ref2", "omit_if_empty": true }
    }
  }
}

Field Mapping

PropertyTypeDescription
from_fieldstringField ID to get value from
pathstringDot notation path into selected item (for select fields)
omit_if_emptybooleanExclude from request if value is empty

Storing Select Field Values

For select fields, store the full selected item object from /options (not just the code). The path property accesses nested values like price.amount or account_number from this object when building the payment request.


Pricing Schema

B2B Data

Pricing information is for your backend only. Do not expose cost, price_adjustment, or discount rates to end users.

Each product includes a pricing object with three fields:

json
{
  "pricing": {
    "cost": {
      "model": "percentage_discount",
      "percentage_rate": 0.98
    },
    "price_adjustment": {
      "type": "fixed",
      "value": 0.5,
      "currency": "MYR"
    },
    "has_loss_risk": false
  }
}
FieldTypeDescription
costobject | nullWholesale cost model
price_adjustmentobject | nullOptional reseller adjustment (fixed or percentage)
has_loss_riskbooleantrue when configured adjustment can produce a loss at some tier

Cost Models

Your wholesale cost is determined by the cost.model:

ModelUse CaseCalculation
percentage_discountMobile reloads, postpaid billscost = price × percentage_rate
fixed_discountUtility bills (TNB, water)cost = price - abs(fixed_amount) (fixed amount can be signed value)

Percentage Discount

json
{
  "cost": {
    "model": "percentage_discount",
    "percentage_rate": 0.97
  }
}

Example: Celcom Postpaid (CB) with 3% discount rate

  • Price RM 100 → Your cost = RM 100 × 0.97 = RM 97

Fixed Discount

json
{
  "cost": {
    "model": "fixed_discount",
    "fixed_amount": { "amount": "-0.50", "currency": "MYR" }
  }
}

Example: TNB with RM 0.50 fixed discount

  • Price RM 100 → Your cost = RM 100 - abs(-0.50) = RM 99.50

Price Adjustment Models

price_adjustment is optional. If absent, no adjustment is applied.

TypeMeaningCalculation
fixedAdd/subtract absolute amountuser_pays = price + value
percentageMultiplier rate (0.95 discount, 1.03 markup)user_pays = price × value
json
{
  "price_adjustment": {
    "type": "percentage",
    "value": 1.03
  }
}

Loss Risk Flag

has_loss_risk is computed by evaluating configured cost and adjustment against denomination boundaries. Use it to warn users when their adjustment may undercut cost.

Pricing Calculation Helper

javascript
function calculatePricing(product, price) {
  const { cost, price_adjustment: adjustment } = product.pricing;

  let yourCost = price;
  if (cost?.model === "percentage_discount") {
    yourCost = price * cost.percentage_rate;
  }
  if (cost?.model === "fixed_discount") {
    yourCost = price - Math.abs(parseFloat(cost.fixed_amount.amount));
  }

  let userPays = price;
  if (adjustment?.type === "fixed") {
    userPays = price + adjustment.value;
  }
  if (adjustment?.type === "percentage") {
    userPays = price * adjustment.value;
  }

  return {
    price,
    yourCost,
    userPays,
    yourMargin: userPays - yourCost,
    hasLossRisk: product.pricing.has_loss_risk,
  };
}

Complete Pricing Example

Face value RM 100, cost.percentage_rate=0.995, price_adjustment.fixed=+0.50:

Price:                RM 100.00
Your Cost:            RM  99.50
Price Adjustment:     RM   0.50
────────────────────────────────
User Pays:            RM 100.50
Your Margin:          RM   1.00

Percentage Values Use Rate Convention

For both cost.percentage_rate and price_adjustment.type=percentage, values are multipliers (for example 0.98, 1.00, 1.03) rather than percentage points.

Option-Level Pricing Data

For options, the API can also return per-item pricing fields in /v2/options responses:

json
{
  "items": [
    {
      "code": "30",
      "label": "RM 30",
      "price": { "amount": "30.00", "currency": "MYR" },
      "cost": { "amount": "29.55", "currency": "MYR" },
      "rrp": { "amount": "30.50", "currency": "MYR" }
    }
  ]
}
FieldTypeDescription
priceMoneyEffective selling price
costMoneyWholesale cost for current reseller
rrpMoneyRecommended retail price

Complete Product Examples

Digi Prepaid — Fixed Amounts (Percentage Discount)

Simple flow: Enter phone → Select amount → Checkout

json
{
  "code": "D",
  "name": "Digi Prepaid",
  "note": "Prepaid reload for Digi mobile numbers",
  "image_url": "https://dashboard.iimmpact.com/img/D.png",
  "processing_time": "instant",

  "fields": [
    {
      "id": "phone",
      "type": "text",
      "input_mode": "tel",
      "label": "Phone Number",
      "placeholder": "e.g. 0123456789",
      "required": true,
      "role": "account",
      "validation": {
        "pattern": "^01[0-9]{8,9}$",
        "message": "Enter valid Malaysian phone number"
      }
    },
    {
      "id": "amount",
      "type": "select",
      "label": "Select Amount",
      "required": true,
      "role": "pricing",
      "data_source": {
        "type": "reference",
        "endpoint": "/options",
        "params": {
          "product_code": { "static": "D" },
          "field_id": { "static": "amount" }
        }
      }
    }
  ],

  "fulfillment": {
    "account": { "from_field": "phone" },
    "amount": { "from_field": "amount", "path": "price.amount" }
  },

  "pricing": {
    "cost": {
      "model": "percentage_discount",
      "percentage_rate": 0.985
    },
    "price_adjustment": null,
    "has_loss_risk": false
  }
}

Pricing: Face value RM 30 → Your cost = RM 30 × 0.985 = RM 29.55 → Margin = RM 0.45

Resulting Payment Request:

json
{
  "refid": "your-unique-refid",
  "product": "D",
  "account": "0123456789",
  "amount": "30.00",
  "extras": {}
}

Flow: Enter phone → Fetch available plans → Select plan → Checkout

json
{
  "code": "HI",
  "name": "Hotlink Internet",
  "note": "Data add-on plans for Hotlink prepaid",
  "image_url": "https://dashboard.iimmpact.com/img/HI.png",
  "processing_time": "instant",

  "fields": [
    {
      "id": "phone",
      "type": "text",
      "input_mode": "tel",
      "label": "Phone Number",
      "placeholder": "e.g. 0123456789",
      "required": true,
      "role": "account",
      "validation": {
        "pattern": "^01[0-9]{8,9}$",
        "message": "Enter valid Malaysian phone number"
      }
    },
    {
      "id": "plan",
      "type": "select",
      "label": "Select Plan",
      "required": true,
      "role": "pricing",
      "data_source": {
        "type": "dynamic",
        "depends_on": ["phone"],
        "endpoint": "/options",
        "params": {
          "product_code": { "static": "HI" },
          "field_id": { "static": "plan" },
          "account_number": { "from_field": "phone" }
        }
      }
    }
  ],

  "fulfillment": {
    "account": { "from_field": "phone" },
    "amount": { "from_field": "plan", "path": "price.amount" },
    "extras": {
      "subproduct_code": { "from_field": "plan", "path": "code" }
    }
  },

  "pricing": {
    "cost": {
      "model": "percentage_discount",
      "percentage_rate": 0.98
    },
    "price_adjustment": {
      "type": "fixed",
      "value": 1.0,
      "currency": "MYR"
    },
    "has_loss_risk": false
  }
}

Resulting Payment Request:

json
{
  "refid": "your-unique-refid",
  "product": "HI",
  "account": "0123456789",
  "amount": "40.00",
  "extras": {
    "subproduct_code": "Unlimited data with hotspot and calls 30-days (3Mbps) H"
  }
}

Pricing: Price RM 40 → Your cost = RM 40 × 0.98 = RM 39.20 → Base margin = RM 0.80.

With fixed price adjustment +1.00, user pays RM 41.00, increasing margin to RM 1.80.


PTPTN — Account Selection + Flexible Amount (Fixed Discount)

Flow: Enter NRIC → Fetch loan accounts → Select account → Enter amount → Checkout

json
{
  "code": "PTPTN",
  "name": "PTPTN",
  "note": "Repay your PTPTN education loan",
  "processing_time": "24_hours",

  "fields": [
    {
      "id": "nric",
      "type": "text",
      "input_mode": "numeric",
      "label": "NRIC Number",
      "placeholder": "Enter 12-digit NRIC",
      "required": true,
      "validation": {
        "pattern": "^[0-9]{12}$",
        "message": "Enter valid 12-digit NRIC"
      }
    },
    {
      "id": "loan_account",
      "type": "select",
      "label": "Select Account",
      "required": true,
      "data_source": {
        "type": "dynamic",
        "depends_on": ["nric"],
        "endpoint": "/options",
        "params": {
          "product_code": { "static": "PTPTN" },
          "field_id": { "static": "loan_account" },
          "account_number": { "from_field": "nric" }
        }
      }
    },
    {
      "id": "amount",
      "type": "money",
      "input_mode": "decimal",
      "label": "Payment Amount",
      "placeholder": "Enter amount",
      "required": true,
      "role": "pricing",
      "currency": "MYR",
      "validation": {
        "min": 10,
        "max": 60000
      }
    }
  ],

  "fulfillment": {
    "account": { "from_field": "loan_account", "path": "account_number" },
    "amount": { "from_field": "amount" },
    "extras": {
      "ic_number": { "from_field": "nric" },
      "subproduct_code": { "from_field": "loan_account", "path": "code" }
    }
  },

  "pricing": {
    "cost": {
      "model": "fixed_discount",
      "fixed_amount": { "amount": "-0.50", "currency": "MYR" }
    },
    "price_adjustment": {
      "type": "percentage",
      "value": 1.01
    },
    "has_loss_risk": false
  }
}

Resulting Payment Request:

json
{
  "refid": "your-unique-refid",
  "product": "PTPTN",
  "account": "009411230450014",
  "amount": "500.00",
  "extras": {
    "ic_number": "941123045001",
    "subproduct_code": "S"
  }
}

Pricing: Price RM 500 → Your cost = RM 500 - abs(-0.50) = RM 499.50 → Base margin = RM 0.50.

With percentage price adjustment 1.01, user pays RM 505.00, increasing margin to RM 5.50.


JomPAY — Biller Selection (Fixed Discount)

Flow: Select biller → Enter references → Enter amount → Checkout

json
{
  "code": "JOMPAY",
  "name": "JomPAY",
  "image_url": "https://dashboard.iimmpact.com/img/JOMPAY.png",
  "processing_time": "instant",

  "fields": [
    {
      "id": "biller",
      "type": "select",
      "label": "Select Biller",
      "required": true,
      "data_source": {
        "type": "reference",
        "endpoint": "/options",
        "params": {
          "product_code": { "static": "JOMPAY" },
          "field_id": { "static": "biller" }
        }
      }
    },
    {
      "id": "ref1",
      "type": "text",
      "label": "Reference 1",
      "placeholder": "Account/Reference number",
      "required": true,
      "role": "account"
    },
    {
      "id": "ref2",
      "type": "text",
      "label": "Reference 2",
      "placeholder": "Optional reference",
      "required": false
    },
    {
      "id": "nric",
      "type": "text",
      "input_mode": "numeric",
      "label": "NRIC Number",
      "placeholder": "Enter 12-digit NRIC",
      "required": true,
      "validation": {
        "pattern": "^[0-9]{12}$",
        "message": "Enter valid 12-digit NRIC"
      }
    },
    {
      "id": "amount",
      "type": "money",
      "input_mode": "decimal",
      "label": "Payment Amount",
      "required": true,
      "role": "pricing",
      "currency": "MYR",
      "validation": {
        "min": 1,
        "max": 30000
      }
    }
  ],

  "fulfillment": {
    "account": { "from_field": "ref1" },
    "amount": { "from_field": "amount" },
    "extras": {
      "biller_code": { "from_field": "biller", "path": "code" },
      "ic_number": { "from_field": "nric" },
      "ref2": { "from_field": "ref2", "omit_if_empty": true }
    }
  },

  "pricing": {
    "cost": {
      "model": "fixed_discount",
      "fixed_amount": { "amount": "-0.30", "currency": "MYR" }
    },
    "price_adjustment": {
      "type": "fixed",
      "value": 0.5,
      "currency": "MYR"
    },
    "has_loss_risk": false
  }
}

Pricing: Face value RM 150 + fixed price adjustment RM 0.50 = User pays RM 150.50 → Your cost = RM 150 - abs(-0.30) = RM 149.70 → Margin = RM 0.80

Dynamic Amount Limits

The min/max in the amount field are fallback values. When user selects a biller, use that biller's min_amount and max_amount from the Options API response to validate the payment amount.


PUBG Mobile — Game Packages

Flow: Enter Player ID → Select package → Checkout

json
{
  "code": "PUBG",
  "name": "PUBG Mobile",
  "note": "Purchase UC for PUBG Mobile",
  "image_url": "https://dashboard.iimmpact.com/img/PUBG.png",
  "processing_time": "instant",

  "fields": [
    {
      "id": "player_id",
      "type": "text",
      "input_mode": "numeric",
      "label": "Player ID",
      "placeholder": "Enter your PUBG Player ID",
      "required": true,
      "role": "account",
      "validation": {
        "pattern": "^[0-9]{8,12}$",
        "message": "Enter valid Player ID"
      }
    },
    {
      "id": "package",
      "type": "select",
      "label": "Select Package",
      "required": true,
      "role": "pricing",
      "data_source": {
        "type": "reference",
        "endpoint": "/options",
        "params": {
          "product_code": { "static": "PUBG" },
          "field_id": { "static": "package" }
        }
      }
    }
  ],

  "fulfillment": {
    "account": { "from_field": "player_id" },
    "amount": { "from_field": "package", "path": "price.amount" },
    "extras": {
      "subproduct_code": { "from_field": "package", "path": "code" }
    }
  },

  "pricing": {
    "cost": {
      "model": "percentage_discount",
      "percentage_rate": 0.945
    },
    "price_adjustment": null,
    "has_loss_risk": false
  }
}

For game products, per-item price / cost / rrp values are returned in /v2/options and should be used for checkout display:

json
{
  "items": [
    {
      "code": "60",
      "label": "60 UC",
      "price": { "amount": "5.00", "currency": "MYR" },
      "cost": { "amount": "4.50", "currency": "MYR" },
      "rrp": { "amount": "5.50", "currency": "MYR" },
      "value": { "amount": 60, "unit": "UC" },
      "option_type": "package"
    },
    {
      "code": "325",
      "label": "325 UC",
      "price": { "amount": "23.00", "currency": "MYR" },
      "cost": { "amount": "19.50", "currency": "MYR" },
      "rrp": { "amount": "25.00", "currency": "MYR" },
      "value": { "amount": 325, "unit": "UC" },
      "option_type": "package"
    }
  ]
}
FieldTypeDescription
priceMoneyBase selling price
costMoneyYour wholesale cost
rrpMoneyOfficial/recommended retail price
valueobjectWhat user receives (amount + unit)
option_typestringSemantic type for UI rendering

Pricing: User buys 60 UC (RM 5) → Your cost = RM 4.50 → Margin = RM 0.50 (RRP is RM 5.50, showing 9% savings)


Caching Strategy

Use Catalog Webhooks for real-time catalog updates. Your backend receives push notifications when the catalog changes — no polling required.

IIMMPACT ──webhook──▶ Your Backend ──serve──▶ Mobile App

                         └── Store in DB (always fresh)

Why Webhooks?

  • Always up-to-date — changes pushed instantly
  • No polling overhead — reduces API calls
  • Single source of truth — your backend controls distribution to clients

Fallback: Pull-Based Sync

If webhooks are not enabled, pull /v2/catalog on a schedule and compare last_updated with your cached copy:

javascript
async function syncCatalog() {
  const response = await fetch("https://api.iimmpact.com/v2/catalog", {
    headers: {
      "X-Api-Key": apiKey,
      "X-Timestamp": timestamp,
      "X-Nonce": nonce,
      "X-Signature": `v1=${signature}`,
    },
  });

  const latest = await response.json();
  const cached = JSON.parse(localStorage.getItem("catalog") ?? "null");

  if (cached?.last_updated === latest.last_updated) {
    return; // unchanged
  }

  localStorage.setItem("catalog", JSON.stringify(latest));
}

Client-Side Caching

If your mobile app fetches directly from IIMMPACT API:

  1. Fetch catalog on app launch
  2. Compare last_updated with cached value
  3. Store catalog in SQLite/local DB for offline access

Error Responses

Validation and application errors typically use this envelope:

json
{
  "message": "The given data was invalid.",
  "errors": {
    "field_name": ["Validation message"]
  }
}

400 Bad Request

Validation errors (for example invalid query parameters):

json
{
  "message": "The given data was invalid.",
  "errors": {
    "is_active": ["The value 'yes' is not valid."]
  }
}

401 Unauthorized

Authentication failed (for example missing HMAC headers, expired timestamp window, nonce reuse, or signature mismatch):

json
{
  "message": "Unauthorized",
  "metadata": {
    "status_code": "401"
  }
}

See API Key Authentication for the complete list of 401 causes.

No-Match Product Filter

If product_code does not match any visible product, the endpoint returns 200 OK with an empty products map while keeping the catalog hierarchy:

json
{
  "last_updated": "2025-01-07T00:00:00Z",
  "tree": {
    "groups": [
      {
        "id": "grp_mobile",
        "name": "Mobile",
        "categories": [
          {
            "id": "cat_prepaid",
            "name": "Prepaid Reload",
            "product_codes": []
          }
        ]
      }
    ]
  },
  "products": {}
}

Handling Errors

For 401 errors, regenerate timestamp/nonce, recompute the signature, and retry with a fresh request. For transient 5xx errors, retry with backoff.

IIMMPACT API Documentation