Curlec
The payment provider — amounts, signatures, and the FPX redirect.
apps/backend/src/modules/curlec/ — registered as a provider of the payment
module, not as a module of its own. Provider identifier: curlec.
Curlec is Razorpay's Malaysian entity, so the API is Razorpay's, and the Razorpay docs apply.
Flow
Order-first, which is Razorpay's model:
- Backend creates a Curlec order for the cart total.
- Storefront opens Curlec's checkout with that order id.
- Customer pays by FPX or card.
- Curlec returns the customer to the return route.
- Backend verifies the signature and authorises the payment session.
Amounts
Sent in sen — multiply by 100.
const amount = Math.round(cartTotal * 100)The provider refuses any currency other than MYR outright. Curlec settles MYR only, and an amount in another currency would be passed through as sen regardless, charging a number that means nothing.
Signature verification
HMAC-SHA256 over order_id|payment_id, keyed with the webhook secret:
const expected = crypto
.createHmac("sha256", secret)
.update(`${razorpay_order_id}|${razorpay_payment_id}`)
.digest("hex")Compare against razorpay_signature. A mismatch means the callback is not
trustworthy and the payment must not be authorised.
FPX needs a redirect
FPX will not work as an inline modal. The checkout must be opened with:
{ callback_url: "<return route>", redirect: true }The storefront's payment button loads
https://checkout.razorpay.com/v1/checkout.js on demand and deliberately passes
no handler — FPX redirects away, so a handler callback never fires and
relying on it silently loses the payment.
Test and live
CURLEC_MODE selects which key pair medusa-config.ts reads:
const CURLEC_MODE = (process.env.CURLEC_MODE ?? 'live') as 'test' | 'live'
const curlecKeyId = (CURLEC_MODE === 'live'
? process.env.CURLEC_LIVE_KEY_ID
: process.env.CURLEC_TEST_KEY_ID) ?? process.env.CURLEC_KEY_IDCurrently live. Changing it is a Railway variable change plus a redeploy.
Webhook
Configured in the Curlec dashboard against the backend. The signing secret is separate from the API key secret — verification uses the webhook secret.