API documentation
Accept FPX, card and e-wallet payments from your own website or system. This guide covers sending a customer to the payment page, receiving the result, querying an order, and the cart API.
1. Overview
There are two ways to integrate. Pick one; both report the result in the same way.
| Payment page | Cart API | |
|---|---|---|
| Best for | You already calculate the order total yourself | You want PaySeller to hold the items, postage and tax |
| How | Your page posts a form to /api/v1/pay | Your server calls /api/v1/cart (JSON) |
| API key | Travels through the customer's browser | Stays on your server |
The flow is the same in both cases: (1) the customer reaches the PaySeller payment page and pays; (2) PaySeller posts the result to your Callback URL; (3) the customer is sent back to your Redirect URL; (4) your server confirms the result with the query API before fulfilling the order.
- Base URL:
https://payseller.my. HTTPS only. - Currency is MYR. Minimum amount per payment is RM 2.00.
- There is no sandbox. Test with a real payment of a small amount.
2. Credentials and settings
Log in to your PaySeller account and open the Websites page. The Merchant ID and API Key appear once your account is verified. Each website has:
| Setting | Use |
|---|---|
| Merchant ID | Sent as merchant_id in every request |
| API Key | Sent as api_key. Treat it as a password |
| Callback URL | Your server endpoint that receives the payment result |
| Redirect URL | The page the customer returns to after paying |
Over transaction limit. Contact PaySeller support to change it.3. Payment page
Send the customer's browser to https://payseller.my/api/v1/pay with the fields below, as a form POST (recommended) or a GET link. PaySeller shows the payment page and takes the customer to the bank, card or e-wallet.
| Field | Required | Description |
|---|---|---|
api_key | Yes | Your API Key |
merchant_id | Yes | Your Merchant ID |
order_no | Yes | Your own order number. Used in the notification and the query API |
order_total | Yes | Amount in MYR, e.g. 198.00. Minimum 2.00 |
payment_code | No | cc card, fpx online banking, ewallet e-wallet. Default is card |
customer_name | No | Shown on the payment page and in receipts |
customer_email | No | The customer's email for the receipt |
customer_country | No | Default Malaysia |
<form method="post" action="https://payseller.my/api/v1/pay">
<input type="hidden" name="api_key" value="YOUR_API_KEY">
<input type="hidden" name="merchant_id" value="YOUR_MERCHANT_ID">
<input type="hidden" name="order_no" value="INV018250">
<input type="hidden" name="order_total" value="198.00">
<input type="hidden" name="customer_name" value="Tan Mei Ling">
<input type="hidden" name="customer_email" value="meiling@example.com">
<input type="hidden" name="payment_code" value="fpx">
<button type="submit">Pay with PaySeller</button>
</form>If the request is rejected, the response is HTTP 200 with a plain-text message instead of the payment page:
| Message | Cause |
|---|---|
API Key Error | api_key and merchant_id do not match an active website |
Account not activated. Please contact PaySeller customer support | api_key is missing or empty |
Minimum transaction RM 2 | order_total is below 2.00 or not a number |
Over transaction limit | order_total is above the website's transaction limit |
- Every load of the payment page creates a new transaction. If a customer tries three times, the query API returns three transactions for the same
order_no. - Because the form passes through the customer's browser, the amount can be altered. Always compare
order_totalin the result with your own order before fulfilling it.
4. Payment notification
When a payment finishes, PaySeller sends an HTTP POST (application/x-www-form-urlencoded) from its server to your Callback URL. It always contains these 10 fields:
| Field | Description |
|---|---|
ord_date | Date of the payment. The format depends on the payment channel, so treat it as text |
order_no | Your order number |
order_total | Amount paid |
payment_type | Credit Card, Bank, TouchNGo, Boost, GrabPay, MayBank QRPay, ShopeePay… |
source_type | Card type, when available |
bank | Bank name for FPX |
bank_reference | Bank or wallet reference number |
raw_error_description | Result text from the bank or gateway |
wcID | Gateway transaction ID |
returncode | Result code, see below |
ord_date=07 November 2026
order_no=INV018250
order_total=198.00
payment_type=Bank
source_type=
bank=Hong Leong Bank Berhad
bank_reference=21739274F0AD
raw_error_description=Approved
wcID=gmhljksc
returncode=100returncode | Meaning |
|---|---|
100 | Paid |
E1 | Failed |
E2 | Cancelled by the customer |
Transaction Incomplete (Pending) | Not final yet. Query again later |
| Anything else | Not paid. The original gateway status |
- Reply with HTTP 200 within 10 seconds. The content of your reply is ignored.
- The notification is sent for failed and cancelled payments too, not only successful ones.
- The same result can arrive more than once. Mark an order as paid only once.
- A notification that cannot be delivered is not sent again. If your server was down, use the query API to catch up.
- Some values may contain
%20in place of spaces. Decodeord_dateandraw_error_descriptionbefore showing them.
returncode 100 and the right amount exists.<?php
// callback.php — receives the payment notification from PaySeller
$orderNo = $_POST['order_no'] ?? '';
// 1. Never trust the notification alone: confirm with the query API.
$ch = curl_init('https://payseller.my/api/v1/query');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'api_key' => 'YOUR_API_KEY',
'merchant_id' => 'YOUR_MERCHANT_ID',
'order_no' => $orderNo,
'format' => 'json',
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
// 2. Paid = any transaction of this order with returncode "100".
$paid = false;
foreach ($data['transactions'] ?? [] as $t) {
if ($t['returncode'] === '100' && (float)$t['order_total'] === expectedTotal($orderNo)) {
$paid = true;
}
}
// 3. Mark the order paid only once (notifications can arrive more than once).
if ($paid) markOrderPaidOnce($orderNo);
http_response_code(200);
echo 'OK';After the notification, the customer's browser is sent to your Redirect URL with a form POST carrying the same 10 fields. Use it only to show a thank-you or a try-again page. Do not fulfil the order from the redirect: the customer may close the browser before it happens, and the fields pass through the browser.
5. Query an order
Call https://payseller.my/api/v1/query from your server, by POST (form fields) or GET, to get the payment result of an order.
| Field | Required | Description |
|---|---|---|
api_key | Yes | Your API Key |
merchant_id | Yes | Your Merchant ID |
order_no | Yes | The order number you sent when paying |
format | Yes | json. Without it the response is an HTML table |
curl -X POST https://payseller.my/api/v1/query \
-d api_key=YOUR_API_KEY \
-d merchant_id=YOUR_MERCHANT_ID \
-d order_no=INV018250 \
-d format=json{
"transactions": [
{
"ord_date": "07%20November%202026",
"order_no": "INV018250",
"order_total": "198.00",
"payment_type": "bank",
"source_type": "",
"bank": "Hong Leong Bank Berhad",
"bank_reference": "21739274F0AD",
"raw_error_description": "Approved",
"wcID": "gmhljksc",
"returncode": "100",
"ord_mercref": "836014_113739"
}
]
}- One entry per payment attempt, newest first. An order that was never sent to the payment page returns
{"transactions":[]}. - The order is paid when any entry has
returncodeequal to100. ord_mercrefis the PaySeller transaction reference. Quote it when you contact support.payment_typehere is a short code (cc,bank,tng,boost,grabpay,shopeepay…), while the notification sends a title (Credit Card,Bank…).- Wrong credentials return HTTP 200 with the plain text
API Key Error, not JSON. Check that the response starts with{before parsing. - Use a 30 second timeout. PaySeller asks the gateway for the latest status of unfinished payments, which can take a few seconds.
- Querying an unfinished payment may trigger another notification to your Callback URL with the latest result.
// Node.js 18+
const res = await fetch("https://payseller.my/api/v1/query", {
method: "POST",
body: new URLSearchParams({
api_key: process.env.PAYSELLER_API_KEY,
merchant_id: process.env.PAYSELLER_MERCHANT_ID,
order_no: "INV018250",
format: "json",
}),
signal: AbortSignal.timeout(30_000),
});
const text = await res.text();
if (!text.startsWith("{")) throw new Error(text); // e.g. "API Key Error"
const paid = JSON.parse(text).transactions.some((t) => t.returncode === "100");6. Cart API
With the cart API your server creates a cart, and PaySeller calculates postage and tax, takes the customer's delivery details and collects the payment. Your API Key never reaches the browser.
- Base URL:
https://payseller.my/api/v1/cart. Send and receive JSON (Content-Type: application/json). - Authenticate with the headers
X-Merchant-IdandX-Api-Key, or withmerchant_idandapi_keyin the JSON body. - Call it from your server only. Limit: 120 requests per minute per merchant.
Create a cart with POST /api/v1/cart:
curl -X POST https://payseller.my/api/v1/cart \
-H "Content-Type: application/json" \
-H "X-Merchant-Id: YOUR_MERCHANT_ID" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{
"order_no": "INV018250",
"items": [
{ "item_id": 1, "quantity": 2, "option_1": "M", "option_2": "Navy" },
{ "title": "Custom engraving", "item_code": "ENG", "price": 15.00, "weight": 0, "quantity": 1 }
],
"customer": {
"name": "Tan Mei Ling", "email": "meiling@example.com", "phone": "0123456789",
"address_line_1": "53, Jalan Mawar 2", "postcode": "81200",
"city": "Johor Bahru", "state": "Johor", "shipping_area": "WM"
},
"redirect_url": "https://example.com/thank-you",
"callback_url": "https://example.com/payseller/callback"
}'| Field | Required | Description |
|---|---|---|
items | Yes | 1 to 50 lines. Use item_id for a product in your PaySeller catalogue (PaySeller's price applies), or title and price for a custom line. quantity is 1 to 99 |
order_no | No | Your order number. If omitted PaySeller generates one. A number that is already paid cannot be reused |
customer | No | Pre-fills the checkout page. shipping_area is WM (West Malaysia), EM (East Malaysia) or SG (Singapore) |
redirect_url | No | Overrides the website's Redirect URL for this cart |
callback_url | No | Overrides the website's Callback URL for this cart |
{
"status": "ok",
"cart_id": "c_OrqeDZ2cmRy-LdB87JZdbQ",
"cart_status": "open",
"checkout_url": "https://payseller.biz.my/123/checkout/c_OrqeDZ2cmRy-LdB87JZdbQ",
"expires_at": "2026-11-18T07:20:00.000Z",
"order_no": "INV018250",
"items": [
{ "line_id": 1, "item_id": 1, "item_code": "A001", "title": "Cotton Tee",
"option_1": "M", "option_2": "Navy", "price": "89.00", "weight": 0.3,
"quantity": 2, "amount": "178.00", "custom": false },
{ "line_id": 2, "item_code": "ENG", "title": "Custom engraving",
"price": "15.00", "weight": 0, "quantity": 1, "amount": "15.00", "custom": true }
],
"subtotal": "193.00"
}Send the customer to checkout_url. They confirm the delivery details, choose a payment method and pay. The cart is valid for 7 days. When shipping_area is known, the response also includes postage, tax and total.
| Method | Path | Use |
|---|---|---|
GET | /api/v1/cart/{cart_id}?shipping_area=WM | Read the cart and its totals |
POST | /api/v1/cart/{cart_id}/items | Add lines (same format as items) |
PATCH | /api/v1/cart/{cart_id}/items/{line_id} | Change quantity: {"quantity": 3}. 0 removes the line |
DELETE | /api/v1/cart/{cart_id}/items/{line_id} | Remove a line |
POST | /api/v1/cart/{cart_id}/checkout | Check out from your server and get a payment link |
DELETE | /api/v1/cart/{cart_id} | Cancel the cart |
If you collect the delivery details yourself, skip the PaySeller checkout page: call checkout with customer, shipping_area and payment_method (fpx, cc or ewallet), then send the customer to pay_url. The link is valid for 24 hours.
curl -X POST https://payseller.my/api/v1/cart/c_OrqeDZ2cmRy-LdB87JZdbQ/checkout \
-H "Content-Type: application/json" \
-H "X-Merchant-Id: YOUR_MERCHANT_ID" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{ "payment_method": "fpx", "shipping_area": "WM" }'
# Response
{ "status": "ok", "cart_id": "c_OrqeDZ2cmRy-LdB87JZdbQ",
"order_no": "INV018250", "total": "201.00",
"pay_url": "https://payseller.my/api/v1/pay.php?pay_token=..." }The result is reported exactly as in sections 4 and 5: the same 10-field notification, and the same query API with your order_no.
Errors return an HTTP 4xx status and a JSON body:
HTTP/1.1 409
{ "status": "error", "code": "OUT_OF_STOCK", "message": "Cotton Tee (M / Navy): only 1 left" }code | Meaning |
|---|---|
AUTH_FAILED | Wrong Merchant ID or API Key |
INVALID_REQUEST | A field is missing or malformed. message says which |
ITEM_NOT_FOUND | The product does not exist or is not on sale |
INVALID_OPTION | The option is not one of the product's options |
INVALID_QUANTITY | Quantity is not 1 to 99 |
INVALID_ITEM | A custom line has no title or an invalid price |
TOO_MANY_ITEMS | More than 50 lines |
CART_NOT_FOUND | No such cart |
CART_EXPIRED | The cart is older than 7 days (HTTP 410) |
CART_CLOSED | The cart is already checked out or cancelled (HTTP 409) |
LINE_NOT_FOUND | No such line_id |
ORDER_NO_USED | This order number is already paid |
AMOUNT_TOO_LOW | The total is below RM 2.00 |
OUT_OF_STOCK | Not enough stock at checkout (HTTP 409). message lists the items |
RATE_LIMITED | Too many requests (HTTP 429) |
7. Before you go live
- The Callback URL and Redirect URL are saved on the Websites page and reachable over HTTPS from the internet.
- Your callback handler confirms every result with the query API before marking an order as paid.
- It compares the paid amount with your own order total.
- It marks an order as paid only once, even when the result arrives several times.
- Orders still unpaid after a while are re-checked with the query API, in case a notification was missed.
- The API Key is stored on your server, not in an app or in JavaScript.
- You have completed one real payment for each payment method you offer, and one cancelled payment.
Need help? Contact PaySeller support with your Merchant ID, the order_no and, when you have it, the ord_mercref.
