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.
- Storefront sends the signed receipt to
POST /store/curlec/returnto resolve the original cart without cookies. - Core cart completion authorizes the existing session by querying Curlec for a matching paid order. A completed cart returns the same order on retry.
POST /store/curlec/return
Requires the publishable API key. Body fields: razorpay_order_id,
razorpay_payment_id, razorpay_signature (64 lowercase hex characters).
Returns { "cart_id": "cart_..." } after verifying the receipt and original
session's provider, amount and MYR currency. Malformed requests return 400;
invalid signatures 401; missing associations 404; session mismatches 409;
missing gateway configuration 503. This endpoint does not charge or create an order.
The storefront next calls core cart completion. Neither path changes cookie security or trusts a client-supplied cart id from the bank callback.
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 API key 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.