We maintain bayarkit, an open-source library with adapters for fourteen Malaysian payment gateways. Writing them meant reading every gateway's documentation, official SDKs and plugins line by line, and running real sandbox payments where we could. This page lists what differs for the developer who integrates them. It does not compare fees, payout times or approval, because we have not measured those.

Every point carries one of two labels: measured (we ran payments on the sandbox in October 2026) or documented (written from official sources, not yet run against the gateway). The sources are in the bayarkit repository, in gateways/registry.json and one spec per gateway.

What to check before choosing

  • Amount unit. Some gateways take integer sen (1050), others ringgit with decimals ("10.50"). Mixing them up charges 100 times too much or too little.
  • Callback signature. Schemes range from HMAC and RSA to MD5 and plain SHA-256. Some sign the raw body, so a body re-serialised from parsed JSON never verifies. One sends only a fixed token.
  • Status query. Can your server ask the gateway for a payment's status, and is that answer signed? Without a query, a replayed callback is hard to detect.
  • Sandbox. Whether one exists, whether it covers FPX, and whether it behaves like the documentation.
  • Callback reply. Some gateways expect an exact reply, such as OK or RECEIVEOK, and retry until they get it.

Measured on the sandbox

toyyibPay

  • Amount in integer sen; RM1.00 is accepted although the documentation says "more than RM1.00". (measured)
  • Callbacks arrive as multipart/form-data, and API responses come back as text/html with leading tabs. (measured)
  • A bill with no transactions, and a wrong secret key, both answer the text No data found!. (measured)
  • We counted seven places where the sandbox differs from the documentation; the bayarkit README lists them.

Billplz

  • Neither the callback nor the redirect carries your reference: find the order by the bill id saved at checkout. (measured)
  • A failed payment still sends a callback, with paid=false. (measured)
  • The X Signature sorts the joined key+value strings, not the keys; the "sort keys" wording in the documentation gives a different HMAC from the official plugin. (documented)

Bayarcash

  • The secret key is shared by every portal in an account, and a transaction names its portal by name. Give each portal a unique name and check it. (measured)
  • The checkout id is a payment intent: read it at /payment-intents/{id}, which answers 201 for a read. (measured)
  • Amounts are ringgit with two decimals ("25.00"). (documented, official SDK)

Stripe (Malaysia)

  • A failed FPX payment sends no checkout.session event: the session stays open until it expires. (measured)
  • The minimum in MYR is RM2.00. Below it, the error blames the payment method types, not the amount. (measured)
  • FPX needs a Malaysian Stripe account, MYR and one-time payments. (documented)

From official documentation, not yet measured

  • CHIP: amounts in sen; callbacks signed with RSA over the raw body; the callback URL must not contain a port, not even :443. Test mode simulates the bank instead of redirecting to one.
  • senangPay: merchants moved to the DOKU platform, so new integrations use the DOKU API with an HMACSHA256= header signature. Amounts in ringgit; a payment can be queried only 60 seconds after it completes.
  • iPay88: the HMAC-SHA512 key is also the first part of the signed message. The status query answers plain text: only the exact string 00 means paid.
  • Fiuu: amounts in ringgit; the callback signature is a double MD5 keyed with the secret key, and the callback currency can arrive as RM instead of MYR.
  • 2C2P: amounts in ringgit with five decimals; only the backend notification is signed (a JWT), so the browser return must never mark an order paid.
  • Razorpay Curlec: amounts in sen; the official Node webhook sample verifies re-serialised JSON, while the signature covers the raw body.
  • HitPay: two signing schemes with two different salts; the sandbox is for Singapore businesses and does not offer FPX.
  • Gkash: the signed amount is the ringgit string without its dot (0.10 becomes 010), and query responses carry no signature.
  • Xendit: webhooks carry a fixed token, not a signature, so check amount and status against your own record or a query. Amounts in ringgit.
  • eGHL: plain SHA-256 over the merchant password and field values; official plugins disagree on which fields are signed.

Patterns across gateways

  • Four official samples (Curlec, HitPay, Airwallex and a 2C2P recipe) contain signature code that can fail on real input. Verify over the raw request bytes, with the key the documentation names.
  • A return URL or a callback is never enough to mark an order paid. A fresh status query catches replayed callbacks.
  • Store amounts as integer sen and convert at the edge for gateways that want ringgit, so a comparison never meets floating-point ringgit.

How this page stays current

Every source behind this page is fingerprinted, and a daily check reports when a gateway's documentation changes. Each change is read by hand before the spec, the test fake or the adapter changes. The FPX walkthrough for Node.js is in How do I accept FPX payments in Node.js?