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.

Endpoint

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

Request Headers

HeaderDescriptionRequired
AuthorizationIdToken (Bearer)Yes

Response Headers

HeaderDescription
Last-ModifiedTimestamp of last catalog update

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",
    "product_type": "standard",
    "image_url": "https://dashboard.iimmpact.com/img/D.png",
    "processing_time": "instant",

    "fields": [...],
    "fulfillment": {...},
    "pricing": {
      "model": "percentage_discount",
      "discount_rate": 1.5
    }
  }
}
FieldTypeDescription
codestringUnique product identifier
namestringDisplay name
product_typestringstandard or custom
image_urlstringProduct logo URL
placeholder_imagestringFallback image identifier
processing_timestringinstant, 24_hours, or 3_days
fieldsarrayForm field definitions
fulfillmentobjectMapping to payment request
pricingobjectWholesale pricing for tenants (B2B)
confirmationobjectPre-checkout confirmation config

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,
    "min_from_field": { "field_id": "biller", "path": "min_amount.amount" },
    "max_from_field": { "field_id": "biller", "path": "max_amount.amount" }
  }
}
PropertyTypeDescription
patternstringRegex pattern for text validation
messagestringError message when validation fails
minnumberMinimum value (for money/number)
maxnumberMaximum value
min_from_fieldobjectDynamic min from another field's selection
max_from_fieldobjectDynamic max from another field's selection

Data Source (Select Fields)

json
{
  "data_source": {
    "type": "dynamic",
    "depends_on": ["phone"],
    "endpoint": "/v2/options",
    "params": {
      "product_code": { "static": "HI" },
      "field_id": { "static": "plan" },
      "account_number": { "from_field": "phone" }
    },
    "cache": {
      "scope": "per_input",
      "ttl_seconds": 300
    },
    "search": {
      "enabled": true,
      "min_chars": 2
    }
  }
}
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
cacheobjectCaching strategy
searchobjectSearch/filter configuration

Cache Scopes

ScopeDescriptionExample
globalSame for all usersDenomination amounts
tenantSame within tenantGame packages with tenant pricing
per_inputUnique per input valuePlans for specific phone number

Fulfillment Schema

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

The fulfillment block only covers product, account, amount, and extras. You still need to provide your own refid (idempotency) and optional remarks when calling /v2/topup.

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

Pricing Schema

B2B Data

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

Each product includes a pricing object with two components:

json
{
  "pricing": {
    "cost": {
      "model": "percentage_discount",
      "percentage_rate": 2.0
    },
    "markup": {
      "model": "fixed",
      "fixed_amount": { "amount": "0.50", "currency": "MYR" }
    }
  }
}
FieldTypeDescription
costobjectYour wholesale cost (what IIMMPACT charges you)
markupobjectYour configured markup (set via Dashboard)

Cost Models

Your wholesale cost is determined by the cost.model:

ModelUse CaseCalculation
percentage_discountMobile reloads, postpaid billscost = face_value × (1 - rate/100)
fixed_discountUtility bills (TNB, water)cost = face_value - fixed_amount
fixed_per_itemGames with variable marginscost = item.cost (from /v2/options)

Percentage Discount

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

Example: Celcom Postpaid (CB) with 3% discount

  • Face value RM 100 → Your cost = RM 100 × (1 - 0.03) = RM 97

Fixed Discount

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

Example: TNB with RM 0.50 fixed discount

  • Face value RM 100 → Your cost = RM 100 - RM 0.50 = RM 99.50

Fixed Per Item

json
{
  "cost": {
    "model": "fixed_per_item"
  }
}

For fixed_per_item products, the cost is specified per item in the /v2/options response:

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" }
    }
  ]
}
FieldTypeDescription
priceMoneyBase selling price
costMoneyYour wholesale cost
rrpMoneyRecommended retail price (optional)
valueobjectWhat user receives (for games, intl top-up)

Markup Models

Your markup is configured via the Dashboard and included in the catalog response:

ModelUse CaseCalculation
noneNo markupuser_pays = face_value
fixed+RM 0.50user_pays = face_value + fixed_amount
percentage+1%user_pays = face_value × (1 + rate/100)

No Markup

json
{
  "markup": {
    "model": "none"
  }
}

Fixed Markup

json
{
  "markup": {
    "model": "fixed",
    "fixed_amount": { "amount": "0.50", "currency": "MYR" }
  }
}

Percentage Markup

json
{
  "markup": {
    "model": "percentage",
    "percentage_rate": 1.0
  }
}

Complete Pricing Example

TNB electricity bill payment for RM 100:

Face Value:           RM 100.00
Your Cost:            RM  99.50  (fixed_discount: -RM 0.50)
Your Markup:          RM   0.50  (fixed: +RM 0.50)
────────────────────────────────
User Pays:            RM 100.50
Your Margin:          RM   1.00  (markup + cost savings)

Pricing Calculation Helper

javascript
function calculatePricing(product, faceValue, selectedItem) {
  const { cost, markup } = product.pricing;

  // Calculate your cost
  let yourCost;
  switch (cost.model) {
    case "percentage_discount":
      yourCost = faceValue * (1 - cost.percentage_rate / 100);
      break;
    case "fixed_discount":
      yourCost = faceValue - parseFloat(cost.fixed_amount.amount);
      break;
    case "fixed_per_item":
      yourCost = parseFloat(selectedItem.cost.amount);
      break;
    default:
      yourCost = faceValue;
  }

  // Calculate user pays (with markup)
  let userPays;
  switch (markup.model) {
    case "fixed":
      userPays = faceValue + parseFloat(markup.fixed_amount.amount);
      break;
    case "percentage":
      userPays = faceValue * (1 + markup.percentage_rate / 100);
      break;
    case "none":
    default:
      userPays = faceValue;
  }

  return {
    faceValue,
    yourCost,
    userPays,
    yourMargin: userPays - yourCost,
  };
}

Complete Product Examples

Digi Prepaid — Fixed Amounts (Percentage Discount)

Simple flow: Enter phone → Select amount → Checkout

json
{
  "code": "D",
  "name": "Digi Prepaid",
  "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": "/v2/options",
        "params": {
          "product_code": { "static": "D" },
          "field_id": { "static": "amount" }
        },
        "cache": { "scope": "global", "ttl_seconds": 86400 }
      }
    }
  ],

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

  "pricing": {
    "cost": {
      "model": "percentage_discount",
      "percentage_rate": 1.5
    },
    "markup": {
      "model": "none"
    }
  }
}

Pricing: Face value RM 30 → Your cost = RM 30 × (1 - 0.015) = 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",
  "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": "/v2/options",
        "params": {
          "product_code": { "static": "HI" },
          "field_id": { "static": "plan" },
          "account_number": { "from_field": "phone" }
        },
        "cache": { "scope": "per_input", "ttl_seconds": 300 }
      }
    }
  ],

  "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": 2.0
    },
    "markup": {
      "model": "fixed",
      "fixed_amount": { "amount": "1.00", "currency": "MYR" }
    }
  }
}

Resulting Payment Request:

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

Pricing: Face value RM 40 + RM 1 markup = User pays RM 41 → Your cost = RM 40 × (1 - 0.02) = RM 39.20 → Margin = 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",
  "placeholder_image": "PTPTN",
  "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": "/v2/options",
        "params": {
          "product_code": { "static": "PTPTN" },
          "field_id": { "static": "loan_account" },
          "account_number": { "from_field": "nric" }
        },
        "cache": { "scope": "per_input", "ttl_seconds": 600 }
      }
    },
    {
      "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" }
    },
    "markup": {
      "model": "percentage",
      "percentage_rate": 1.0
    }
  }
}

Resulting Payment Request:

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

Pricing: Face value RM 500 + 1% markup = User pays RM 505 → Your cost = RM 500 - RM 0.50 = RM 499.50 → Margin = 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": "/v2/options",
        "params": {
          "product_code": { "static": "JOMPAY" },
          "field_id": { "static": "biller" }
        },
        "cache": { "scope": "global", "ttl_seconds": 86400 },
        "search": { "enabled": true, "min_chars": 2 }
      }
    },
    {
      "id": "ref1",
      "type": "text",
      "label": "Reference 1",
      "placeholder": "Account/Reference number",
      "required": true,
      "role": "account",
      "validation": { "max_length": 30 }
    },
    {
      "id": "ref2",
      "type": "text",
      "label": "Reference 2",
      "placeholder": "Optional reference",
      "required": false,
      "validation": { "max_length": 30 }
    },
    {
      "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,
        "min_from_field": { "field_id": "biller", "path": "min_amount.amount" },
        "max_from_field": { "field_id": "biller", "path": "max_amount.amount" }
      }
    }
  ],

  "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" }
    },
    "markup": {
      "model": "fixed",
      "fixed_amount": { "amount": "0.50", "currency": "MYR" }
    }
  }
}

Pricing: Face value RM 150 + RM 0.50 markup = User pays RM 150.50 → Your cost = RM 150 - RM 0.30 = RM 149.70 → Margin = RM 0.80


PUBG Mobile — Game Packages (Fixed Per Item)

Flow: Enter Player ID → Select package → Checkout

json
{
  "code": "PUBG",
  "name": "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": "/v2/options",
        "params": {
          "product_code": { "static": "PUBG" },
          "field_id": { "static": "package" }
        },
        "cache": { "scope": "global", "ttl_seconds": 86400 }
      }
    }
  ],

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

  "pricing": {
    "cost": {
      "model": "fixed_per_item"
    },
    "markup": {
      "model": "none"
    }
  }
}

For fixed_per_item products, your cost varies per item and is returned in the /v2/options response:

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" }
    },
    {
      "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" }
    }
  ]
}
FieldTypeDescription
priceMoneyBase selling price
costMoneyYour wholesale cost
rrpMoneyOfficial/recommended retail price
valueobjectWhat user receives (amount + unit)

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


Custom Products

Some products cannot be expressed with the standard field model and use product_type: "custom". These require hardcoded flows in your application.

json
{
  "code": "EVD",
  "name": "Every Pay",
  "product_type": "custom",
  "image_url": "https://dashboard.iimmpact.com/img/EVD.png"
}

Handling Custom Products

javascript
function renderProductForm(product) {
  // Check for custom products first
  if (product.product_type === "custom") {
    switch (product.code) {
      case "EVD":
        return <EveryPayFlow product={product} />;
      // Add more custom product handlers as needed
      default:
        // Unknown custom product - show unsupported message
        return <UnsupportedProduct product={product} />;
    }
  }

  // Standard products use dynamic form
  return <DynamicProductForm product={product} />;
}

Current Custom Products

CodeNameReason for Custom Flow
EVDEvery PayComplex multi-step payment flow

Future Custom Products

When new custom products are added, your app will need an update to support them. Subscribe to our changelog for advance notice of new custom products.


Caching Strategy

Use 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: Conditional Requests

For direct client-to-API access or initial bootstrap, use conditional requests:

javascript
async function syncCatalog() {
  const headers = { Authorization: `Bearer ${idToken}` };

  const lastModified = localStorage.getItem("catalog_last_modified");
  if (lastModified) {
    headers["If-Modified-Since"] = lastModified;
  }

  const response = await fetch("https://api.iimmpact.com/v2/catalog", {
    headers,
  });

  if (response.status === 304) {
    return; // Not modified, use cached
  }

  const catalog = await response.json();
  localStorage.setItem("catalog", JSON.stringify(catalog));
  localStorage.setItem(
    "catalog_last_modified",
    response.headers.get("Last-Modified"),
  );
}

Client-Side Caching

If your mobile app fetches directly from IIMMPACT API:

  1. Fetch catalog on app launch
  2. Use If-Modified-Since for subsequent requests
  3. Store in SQLite/local DB for offline access

Error Responses

401 Unauthorized

Invalid or expired bearer token:

json
{
  "error": "unauthorized",
  "message": "Invalid or expired bearer token"
}

403 Forbidden

Tenant does not have access to the catalog:

json
{
  "error": "forbidden",
  "message": "Tenant not authorized for catalog access"
}

503 Service Unavailable

Catalog temporarily unavailable:

json
{
  "error": "service_unavailable",
  "message": "Catalog temporarily unavailable",
  "retry_after": 30
}

Handling Errors

When receiving a 503 error, use the retry_after value (in seconds) before retrying. For 401 errors, refresh your bearer token and retry.

IIMMPACT API Documentation