SRS.my Partner API v1
Sell reloads, PINs, bill payments, game credit and international top-ups from your own system, and read your balance, prices, transactions and sales reports. REST over HTTPS, JSON responses, callbacks when a result is final.
Getting started
- Log in to the SRS.my portal and open API Keys. Create a test key (
sk_test_…). - Build against the sandbox: test keys never charge your balance. Each test key has its own RM1000 sandbox balance (reset it on the API Keys page).
- When you are ready, click Request live access. After approval, create a live key (
sk_live_…). The same code works; live keys charge your SRS.my balance at your own rates.
Base URL: https://srs.my/v1. HTTPS only. Each key may make up to 20 requests per second.
Authentication
Send the key in the Authorization header on every request:
Authorization: Bearer sk_live_3f9a…
A key is shown only once when it is created; SRS.my keeps only a fingerprint of it. Lost a key? Revoke it and create a new one. On the API Keys page you can also restrict a key to your server's IP addresses.
All endpoints
| Endpoint | Does |
|---|---|
POST /v1/topup | Buy any product: reload, PIN, bill, game credit, council payment, international top-up. |
POST /v1/topup/batch | Up to 10 top-ups in one request. |
GET /v1/status | Result of one transaction or up to 50 at once, with the telco reference and the PIN. |
GET /v1/transactions | Your latest transactions, or only the refunded ones. |
GET /v1/balance | Your balance. |
GET /v1/products | What you can buy, with your discount, rebate tiers and price. |
GET /v1/sales | Sales, cost and profit per product between two dates. |
Parameters go in the query string for GET, and as form fields or a JSON object body for POST. Lists take limit (1 to 100, default 10).
Top up
POST /v1/topup — form fields or a JSON object body.
| Field | Required | |
|---|---|---|
product | yes | Product code from /v1/products, e.g. MAXIS. |
msisdn | yes | What to top up or pay: a phone number, bill account number or game id, depending on the product (table below). account is accepted as another name for this field. |
amount | yes | Face value in whole ringgit. Must be one of the product's amounts or ranges. |
customer_mobile | no | Bills, games and council payments only: your customer's Malaysian mobile number (0123456789 or 60123456789), passed on with the payment. |
client_txn_id | recommended | Your own reference, up to 40 letters, digits or _ . : -. See retries. |
An accepted request is charged at once and queued to the network. The answer is PENDING; the final result comes by callback or /v1/status, usually within seconds.
HTTP/1.1 200 OK
{"status":"PENDING","txn_id":"20261005103012345128","client_txn_id":"ORDER-1001","mode":"live",
"product":"MAXIS","msisdn":"0123456789","amount":30,"price":31.05,"balance":4812.40}
A refused request answers 422 with an error code; nothing is charged.
Several top-ups in one request
POST /v1/topup/batch — a JSON body with up to 10 top-ups. Each line takes the same fields as a single /v1/topup.
curl -X POST https://srs.my/v1/topup/batch -H "Authorization: Bearer sk_test_..." -H "Content-Type: application/json" \
-d '{"items":[
{"product":"MAXIS","msisdn":"0123456789","amount":10,"client_txn_id":"ORDER-1001"},
{"product":"DIGI","msisdn":"0161234567","amount":5,"client_txn_id":"ORDER-1002"}
]}'
HTTP/1.1 200 OK
{"mode":"live","count":2,"accepted":1,"refused":1,"balance":4802.05,
"results":[
{"status":"PENDING","txn_id":"20261005103012345128","client_txn_id":"ORDER-1001","mode":"live","product":"MAXIS",
"msisdn":"0123456789","amount":10,"price":10.35,"balance":4802.05,"line":1},
{"status":"ERROR","code":"INVALID_AMOUNT","message":"Amounts on sale for DIGI: 5-50","client_txn_id":"ORDER-1002","line":2}
]}
- Each line stands alone. A line that is refused does not stop or undo the others; the answer is
200and you read every entry ofresults.lineis its position in your list, starting at 1. - An accepted line is
PENDINGlike a single top-up: its result comes by /v1/status (you can ask for all of them at once withclient_txn_ids) or by callback. - Give every line its own
client_txn_id. If the request times out, send the same batch again: lines already taken answer with"replayed":trueand are not charged twice. - More than 10 lines answers
422 TOO_MANYand nothing is processed. Send several requests.
Product types
One endpoint sells every product. The product's category in /v1/products decides what msisdn must hold; the same rules apply as on the portal's page for that category.
| category | msisdn holds | Notes |
|---|---|---|
RELOAD, POSTPAID, DATA, PRODUCT | Phone number, digits only. | |
PIN | Your customer's phone number, at least 10 digits. | The PIN comes back in /v1/status and in the callback. Some PIN products close for some hours of the day: PRODUCT_CLOSED. |
BILL | Bill account number, 7 to 18 letters, digits or _ . - | Send customer_mobile. Your balance must cover the full bill amount. |
GAMES | Game or player id, 8 to 16 characters. Mobile Legends: GameID_ZoneID. | As for bills. |
LOCALCOUNCIL | Council account number, 8 to 16 characters. | As for bills. |
INTERNATIONAL | Phone number with its country code, digits only. |
Each product also has its own length limits: msisdn_length in /v1/products. Products of other categories are not sold through this API yet.
curl -X POST https://srs.my/v1/topup -H "Authorization: Bearer sk_test_..." \
-d product=TNB -d msisdn=220012345678 -d amount=120 -d customer_mobile=0123456789 -d client_txn_id=BILL-88
Retries and client_txn_id
If a request times out, send it again with the same client_txn_id. You get the first request's current state with "replayed":true and are never charged twice. Using the same client_txn_id for a different number or amount answers 409 CLIENT_TXN_ID_REUSED.
Separately, the same number, product and amount within 30 minutes of a successful or pending reload is refused as DUPLICATE_REQUEST, whichever channel sent the first one.
Transaction status
GET /v1/status?txn_id=… or /v1/status?client_txn_id=…
{"txn_id":"20261005103012345128","client_txn_id":"ORDER-1001","mode":"live","status":"SUCCESS","raw_status":"SUCCESS",
"error":"","product":"MAXIS","msisdn":"0123456789","amount":30,"price":31.05,"reference":"MX88213345","pin":null,"created":"2026-10-05T10:30:12"}
PIN products
For a product of category PIN, pin holds the PIN once one is assigned (normally when the status turns SUCCESS); until then, and for every other product, it is null.
"pin":{"serial":"8800123456","pin":"1234567890123456","expiry":"2026-12-31","instruction":"Dial *111*PIN#"}
Treat the PIN like cash: anyone who reads it can use it. expiry is shown at most 90 days ahead.
Several at once
GET /v1/status?txn_ids=a,b,c or /v1/status?client_txn_ids=a,b,c — up to 50 ids, separated by commas.
{"mode":"live","transactions":[{"txn_id":"20261005103012345128","status":"SUCCESS", …},
{"client_txn_id":"ORDER-9","status":"NOT_FOUND"}]}
Each entry has the fields of a single status answer. An id that is not yours, or does not exist, comes back as NOT_FOUND instead of failing the whole request.
| status | |
|---|---|
PENDING | Charged and with the network. Wait for the callback or poll every few seconds. |
SUCCESS | Delivered. reference is the network's reference when available. |
FAILED | Not delivered (refused, failed or refunded). raw_status and error give the detail. A refunded amount returns to your balance. |
Live keys can look up any of your transactions, including those made on the portal or by SMS.
Recent transactions
GET /v1/transactions — your latest requests from every channel, newest first. limit sets how many. Add status=refunded for the refunded ones only, to reconcile refunds.
{"mode":"live","transactions":[{"txn_id":"20261005103012345128","status":"SUCCESS","raw_status":"SUCCESS","error":"",
"product":"MAXIS","msisdn":"0123456789","amount":30,"created":"2026-10-05T10:30:12"}]}
The list covers recent days. Use /v1/status for the reference, the PIN, or an older transaction.
Balance
GET /v1/balance → {"balance":4812.40,"currency":"MYR","mode":"live"} (the sandbox balance for a test key).
Products and your prices
GET /v1/products lists what your account can buy, with your price for each fixed amount.
{"mode":"live","products":[{"product":"MAXIS","category":"RELOAD","description":"Maxis prepaid",
"amounts":[5,10,30],"ranges":[],"prices":[{"amount":5,"price":5.18},{"amount":10,"price":10.35},{"amount":30,"price":31.05}],
"msisdn_length":{"min":10,"max":11},"msisdn_digits_only":true,"customer_mobile":false,"returns_pin":false,
"discount":2.0000,"rebates":[{"amounts":"5-30","rebate":0.5000}],"discount_type":"percent","tax_percent":6.00,
"first_reload_per_month_only":false,"product_id":"1"}]}
Add ?category=PIN (or any other category) to list one category. discount is your rate, a percentage or a ringgit amount per discount_type. rebates are extra discount for the listed amounts ("5-30" is a range, "50" one amount); the first tier that fits is added to discount, and prices already include it. product_id is the number the SOAP API calls sProductID. Every product SRS.my sells, with its ID, amounts and number length, is in the Excel file behind Product list (Excel) on the API Keys page.
ranges lists open amounts (any whole ringgit from min to max). When first_reload_per_month_only is true, the discount applies to the first reload of a number in 30 days; later ones are charged at face value plus tax.
Sales report
GET /v1/sales?from=2026-10-01&to=2026-10-05 — successful sales per product. Both dates are included; leave them out for today. At most 31 days per request.
{"mode":"live","from":"2026-10-01","to":"2026-10-05","count":212,"total":4310,"cost":4226.35,"profit":83.65,
"products":[{"product":"MAXIS","count":140,"total":2800,"cost":2744.00,"profit":56.00}, …]}
total is the face value sold, cost what your balance was charged, and profit the retail price minus the cost.
Webhooks
Set a callback URL for each key on the API Keys page (https; http is allowed for test keys). SRS.my POSTs JSON to it:
| X-SRS-Event | When |
|---|---|
topup.updated | An API transaction became SUCCESS or FAILED (again if it changes later, e.g. refunded). Same fields as /v1/status, plus event and sent; a sold PIN is in pin. |
deposit.credited | A deposit was credited to your balance. Live keys only. Fields: amount, balance, remarks, credited. |
ping | "Send test webhook" on the API Keys page. |
Answer with any 2xx status. Otherwise SRS.my retries after 1, 2, 4… up to 60 minutes, 10 times. Callbacks are sent within about a minute of the change; use txn_id to ignore repeats.
Every callback carries X-SRS-Signature: the HMAC-SHA256 of the raw request body with your key's webhook secret, in lowercase hex. Check it before trusting the body:
// PHP
$body = file_get_contents('php://input');
$ok = hash_equals(hash_hmac('sha256', $body, $webhookSecret), $_SERVER['HTTP_X_SRS_SIGNATURE'] ?? '');
// Node.js (Express: app.use(express.raw({ type: 'application/json' })))
const crypto = require('crypto');
const expected = crypto.createHmac('sha256', webhookSecret).update(req.body).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.get('X-SRS-Signature') || ''));
# Python
import hmac, hashlib
ok = hmac.compare_digest(hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest(), signature_header)
Sandbox numbers
| msisdn ends with | Result |
|---|---|
0000 | FAILED (TEST_DECLINED), not charged |
9999 | stays PENDING |
| anything else | SUCCESS about 3 seconds after the request |
Sandbox prices, amounts and number lengths follow your real product list, so a request that works in the sandbox works live. The rules above apply to every product type, by the last digits of msisdn.
| With a test key | |
|---|---|
| PIN products | A successful purchase returns the made-up PIN 1234567890123456. |
/v1/transactions, /v1/sales | Cover the requests made with that test key. |
Errors
Errors answer {"status":"ERROR","code":"…","message":"…"}.
| HTTP | code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, unknown or revoked key. |
| 403 | LIVE_NOT_APPROVED | Live access is not approved for this account. |
| 403 | IP_NOT_ALLOWED | The key is restricted to other IP addresses. |
| 403 | ACCOUNT_INACTIVE, HTTPS_REQUIRED | Account suspended; request sent over http. |
| 404 | TXN_NOT_FOUND, NOT_FOUND | No such transaction for this account and mode; unknown endpoint. |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method. |
| 409 | CLIENT_TXN_ID_REUSED | client_txn_id already used for a different request. |
| 422 | MISSING_PARAMETER, INVALID_MSISDN, INVALID_AMOUNT, INVALID_CLIENT_TXN_ID, PRODUCT_NOT_AVAILABLE, INVALID_CUSTOMER_MOBILE, PRODUCT_CLOSED | Request rejected before anything was charged. |
| 422 | TOO_MANY (more than 50 ids, or more than 10 top-ups in a batch), INVALID_LIMIT, INVALID_DATE, INVALID_DATE_RANGE | A list or report parameter is wrong; the message says which. |
| 422 | INSUFFICIENT_BALANCE, PRODUCT_NOT_ALLOWED, DUPLICATE_REQUEST, REFUSED | Refused, not charged. Includes txn_id. |
| 429 | RATE_LIMITED | More than 20 requests in one second. |
| 500 | SYSTEM_ERROR | Check /v1/status with your client_txn_id, then retry with the same client_txn_id. |
Moving from the SOAP API
The SOAP API at https://srs.my/srsapi/connect.asmx keeps working, unchanged, on the same account and balance. You can move one call at a time; a transaction made through either API is visible in both.
| SOAP method | REST |
|---|---|
RequestTopup, RequestBuy, RequestPIN | POST /v1/topup |
CheckTransactionStatus, CheckTransactionStatusRazer, GetReloadPIN | GET /v1/status |
CheckTransactionStatusBatch | GET /v1/status?txn_ids=… |
GetRecentTransaction, GetRecentTransactionRefunded | GET /v1/transactions, with status=refunded |
CheckBalance | GET /v1/balance |
GetProduct, GetAgentProductDiscount, GetAgentProductRebate | GET /v1/products |
GetAgentSales, GetAgentSalesProfit | GET /v1/sales |
| What changes | SOAP | REST |
|---|---|---|
| Sign-in | Mobile number, password and an MD5 sEncKey in every call. | One Authorization: Bearer key. Your password is never sent; a key can be revoked and tied to your IP addresses. |
| Product | sProductID (a number) | product (the code, e.g. MAXIS). /v1/products shows both. |
| Target | sCustomerAccountNumber, sCustomerMobileNumber, sOtherParameter | msisdn, and customer_mobile for bills. |
| Your reference | sClientTxID | client_txn_id. A repeat never charges twice, and says so with "replayed":true. |
| Result | Polling only. Status words: PENDING, PROCESSING, COMPLETED, SUCCESS, REFUNDED, BANNED. | Callback to your server, or polling. Three states: PENDING, SUCCESS, FAILED; the original word is in raw_status. |
| Errors | A status number and text inside a 200 answer. | HTTP status plus a fixed code. |
Other SOAP methods have no REST version; keep calling them on the SOAP API.
Code samples
cURL
curl -X POST https://srs.my/v1/topup \
-H "Authorization: Bearer sk_test_..." \
-d product=MAXIS -d msisdn=0123456789 -d amount=30 -d client_txn_id=ORDER-1001
PHP
$ch = curl_init('https://srs.my/v1/topup');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
CURLOPT_POSTFIELDS => http_build_query(['product' => 'MAXIS', 'msisdn' => '0123456789', 'amount' => 30, 'client_txn_id' => 'ORDER-1001']),
]);
$result = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Node.js (18+)
const res = await fetch('https://srs.my/v1/topup', {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ product: 'MAXIS', msisdn: '0123456789', amount: 30, client_txn_id: 'ORDER-1001' }),
});
const result = await res.json();
Python
import requests
r = requests.post('https://srs.my/v1/topup',
headers={'Authorization': f'Bearer {api_key}'},
data={'product': 'MAXIS', 'msisdn': '0123456789', 'amount': 30, 'client_txn_id': 'ORDER-1001'},
timeout=30)
result = r.json()
Questions: WhatsApp 012-933 6318.