Appearance
Make Payment
Process transactions across the IIMMPACT platform. This is a unified endpoint for all product offerings, including JomPAY, mobile reload, gift cards, vouchers, eSIM purchases, and more.
TIP
For a step-by-step integration walkthrough, see the Make Payment Guide.
Idempotency
Once a transaction is created, its refid is permanent: resending the same request returns that transaction's current status and never creates a duplicate. Store and re-use the same refid for the same transaction.
Check Final Transaction Status: Call this API again with the identical request parameters and the original refid used during initiation. The API will return the current status of the transaction. This works for any refid that created a transaction (the response has a transaction_id). After a rejection (HTTP 400 without transaction_id) nothing was created, so resending that refid is a new purchase attempt.
API Endpoint
http
POST https://api.iimmpact.com/v2/topupDANGER
Staging server statuses and failure reasons might not match production due to limited validation.
Request Headers
| Header | Description | Required |
|---|---|---|
X-Api-Key | Your API key | Yes |
X-Timestamp | Unix timestamp in seconds | Yes |
X-Nonce | Unique request identifier | Yes |
X-Signature | HMAC-SHA256 signature (v1=...) | Yes |
X-API-Version | API version date for opting into upcoming response format changes (e.g. 2026-04-01). Dates later than tomorrow (UTC) and malformed values are ignored. Optional. | No |
See API Key Authentication for how to sign your requests.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
refid | string | Yes | Your unique reference ID for this transaction. See refid constraints. |
product | string | Yes | Product code |
remarks | string | No | Optional field for any additional remarks |
account | string | Yes | Recipient's account number or identifier (e.g., bill account number, mobile number) |
amount | string | number | Yes | Transaction face value or quantity. For a pricing select, send the selected option's numeric denomination, not price.amount; for a direct money field, send the entered amount. Internally parsed as decimal. The response always returns a number (integer if whole, decimal otherwise). |
extras | object | No | Additional parameters required by specific products (see below) |
extras.subproduct_code | string | Conditional | Exact option code from /v2/options for the product's pricing field. Required for mobile internet products. Must correspond to the plan whose denomination equals amount. |
extras.ic_number | string | No | IC number without dashes |
extras.biller_code | string | No | Biller code |
extras.ref2 | string | Conditional | Required by some JomPAY billers. The ref2 value is typically printed on the user's JomPAY bill. Use bill presentment to validate. The API returns an error if ref2 is required but not provided. |
refid Constraints
| Constraint | Value |
|---|---|
| Max length | 50 characters |
| Allowed characters | Any non-empty string |
| Uniqueness scope | Per reseller account (not global) |
| Idempotency | Permanent — re-querying with an existing refid always returns the original transaction |
Example Request Body
jsonc
{
"refid": "321479-0-30f4f209-ee",
"product": "GC",
"account": "0123456789",
"amount": 10.00, // Strings also work, e.g. "10.00"
"remarks": "",
"extras": {}
}Extras Requirements by Product
| Parameter | JomPAY | PTPTN | Mobile Data | Others |
|---|---|---|---|---|
subproduct_code | - | Mandatory | Mandatory | - |
ic_number | Mandatory (payee IC) | Mandatory (recipient IC) | - | - |
biller_code | Mandatory | - | - | - |
ref2 | Conditional | - | - | - |
Plan code validation
A supplied plan code must exactly match (case-sensitive) an active option code for the product's pricing field. The option's denomination must equal amount.
Validation failures return HTTP 400 with data.statusCode 44, create no transaction, and leave the refid available for reuse. The possible remarks are:
Multiple plans are available for this amount. Please provide a plan code.unknown or inactive option code '<code>'. Select an available plan and try again.option code '<code>' does not match amount <amount>. Select the plan for this amount and try again.
This validation does not depend on X-API-Version.
Backward compatibility
Existing integrations that send amount only continue to work while the amount identifies exactly one active plan. New integrations must send subproduct_code.
JomPAY Testing
For staging test biller codes that simulate JomPAY payment outcomes, see JomPAY Test Biller Codes.
AMLA Compliance — JomPAY
Due to AMLA (Anti-Money Laundering Act 2001) requirements, it is mandatory to perform eKYC (electronic Know Your Customer) on your end-users for all JomPAY transactions. Pass the verified IC number (Malaysians) or Passport number (non-Malaysians) via the ic_number field. Failure to comply, or providing fictitious/invalid numbers, may lead to account suspension.
Response 200
| Field | Type | Description |
|---|---|---|
data | object | Transaction result |
data.statusCode | number | Status code — see Payment Status Codes |
data.status | string | Outcome: Accepted, Processing, Succesful, or Failed |
data.transaction_id | string | IIMMPACT transaction ID. Present on every response that describes a transaction; omitted on rejected requests |
data.account | string | Account number |
data.product | string | Product code |
data.productName | string | Product name |
data.amount | number | Amount paid |
data.sn | string | Serial number from the provider/operator |
data.pin | string | PIN for vouchers, gift cards, etc. |
data.expiry | string | Voucher expiration date (yyyymmdd) |
data.cost | number | Your wholesale cost for this transaction (deducted from your IIMMPACT wallet balance). |
data.balance | number | Current wallet balance |
data.remarks | string | Additional notes about the transaction |
data.refid | string | Your unique reference ID |
data.timestamp | string | Transaction timestamp |
data.note | string | Instructions or information for the user regarding the product |
data.voucherlink | string | Link to access or redeem the voucher |
Spelling Note
The success status value is spelled Succesful (single 's'), not Successful. This is a legacy quirk that cannot be changed. Match this exact string in your code.
Example Response:
json
{
"data": {
"statusCode": 20,
"status": "Succesful",
"transaction_id": "123871821",
"account": "0123456789",
"product": "GC",
"productName": "Grab Gift Code",
"amount": 5,
"sn": "106648697",
"pin": "MPHE39G3WL",
"expiry": "20251116",
"cost": 5,
"balance": 50.54,
"remarks": "",
"refid": "321479-0-30f4f209-ee",
"timestamp": "2025-05-20 13:09:34",
"note": "Insert voucher code into Use Grab Gifts under Use Offers section upon check out",
"voucherlink": "https://api.grab.com/gifts/v2/go?id=7464957319334bb0afbd5980738a2b50"
}
}Response 400
Rejected requests (invalid product, insufficient balance, invalid denomination, invalid account, a temporary system error, etc.) return HTTP 400 with the same {"data": {...}} body shape as HTTP 200, without a transaction_id.
A transaction that fails as soon as it is created can also come back with HTTP 400 (or 5xx), but then the response carries its transaction_id. Treat any response with a transaction_id as that transaction's status, not as a rejection.
Key Difference from HTTP 200
HTTP 400 without transaction_id means no transaction was created and the refid is not consumed. You can safely retry with the same refid after fixing the cause: top up for 43, correct the request for 44/58, or wait a few seconds for 49. This is different from an HTTP 200 Failed response, where a transaction was created. See Not processed.
| Field | Type | Description |
|---|---|---|
data | object | Validation error result |
data.statusCode | number | Status code — see Payment Status Codes |
data.status | string | Always Failed for validation errors |
data.account | string | Account number from the request |
data.product | string | Product code from the request |
data.amount | number | Amount from the request |
data.cost | number | Always 0 (no transaction was processed) |
data.balance | number | Current wallet balance (unchanged) |
data.remarks | string | Reason for the validation failure (e.g., Invalid_Denomination, Insufficient_Balance) |
data.refid | string | Your reference ID (not consumed — can be reused) |
data.timestamp | string | Timestamp of the response |
Example Response:
json
{
"data": {
"statusCode": 58,
"status": "Failed",
"account": "0123456789",
"product": "DB",
"amount": 22.23,
"cost": 0,
"balance": 50.54,
"remarks": "Invalid_Denomination",
"refid": "my-ref-id",
"timestamp": "2026-03-16 13:09:34"
}
}Transaction Status Values
| HTTP | Status | Final? | Transaction Created? | What To Do |
|---|---|---|---|---|
400, no transaction_id | Failed | No — request rejected | No | Fix the cause, retry with same refid (or close the order and never reuse the refid) |
| 200 | Accepted | No | Yes | Re-query with same refid |
| 200 | Processing | No | Yes | Re-query with same refid |
| 200 | Succesful | Yes | Yes | Done |
| 200 | Failed | Yes | Yes | Final failure — use new refid |
| 200 | Failed, statusCode 53, remarks Refund | Yes | Yes | Transaction was voided |
Distinguish HTTP 400 Failed vs HTTP 200 Failed
- HTTP 400 +
Failedwithouttransaction_id= no transaction was created,refidnot consumed. Retry with the samerefidafter fixing the cause. - Any response with
transaction_id(HTTP 200, 400 or 5xx) = a transaction exists. Follow itsstatus; aFailedtransaction is final — use a newrefidfor the next attempt. - HTTP 200 +
Failed= a transaction was created and failed at the provider level. This is final — use a newrefidfor the next attempt.
Recommended Polling Strategy
When a transaction returns Processing, poll every 5-10 seconds by re-querying with the same refid. Most transactions resolve within 30 seconds. Always combine polling with callbacks — polling provides immediate UX feedback, while callbacks are the reliable final status delivery mechanism.
