Overview
Icon for CutLuy

CutLuy

Accept Bakong KHQR scan-to-pay payments via CutLuy

medusa-payment-cutluy

A Medusa Payment Module Provider for CutLuy — Bakong KHQR scan-to-pay payments.

CutLuy is an asynchronous payment method: the customer scans a KHQR code (or opens a hosted checkout page) and pays from their banking app. The plugin is a standalone Medusa payment provider (like Copy to clipboardmedusa-payment-stripe) and installable in any Medusa v2 application.

Features

  • ✅ Create a CutLuy KHQR payment per payment session
  • ✅ Exposes Copy to clipboardqr_string and Copy to clipboardcheckout_url to the storefront through the payment session Copy to clipboarddata
  • ✅ Asynchronous authorization (Copy to clipboardpending_authorization) → order created as "awaiting"
  • ✅ Webhook handling via Medusa's built-in listener with HMAC-SHA256 signature verification (over the raw body) and session/amount verification
  • ✅ Scheduled sweep job that detects CutLuy payments expired/failed while their order is still Copy to clipboardawaiting (dropped webhook events) and flags them for operators
  • Copy to clipboardpayment.completed webhook marks the session captured and completes the cart/order
  • ✅ Poll-based status (Copy to clipboardgetPaymentStatus) and retrieval (Copy to clipboardretrievePayment)
  • ✅ USD-only enforcement (CutLuy only charges USD)
  • ❌ Capture / cancel / delete / update / refund are not part of CutLuy's public v1 API — implemented as safe no-ops or explicit errors

How it works

sequenceDiagram
participant S as Storefront
participant M as Medusa Backend
participant C as CutLuy
S->>M: initiate payment session (provider_id = cutluy)
M->>C: POST /v1/payments (amount in USD, metadata.session_id)
C-->>M: payment { id, qr_string, checkout_url }
M-->>S: payment session data (qr_string / checkout_url)
S->>S: render QR or redirect to checkout_url
S->>M: place order → authorizePayment → pending_authorization
C->>M: webhook payment.completed → /hooks/payment/cutluy_cutluy
M->>M: verify X-CutLuy-Signature → mark captured → complete cart/order

Because Copy to clipboardauthorizePayment returns Copy to clipboardpending_authorization, the order is created with an awaiting payment status. When CutLuy fires Copy to clipboardpayment.completed, Medusa's Copy to clipboardprocessPaymentWorkflow re-invokes Copy to clipboardauthorizePayment — the provider re-checks the CutLuy payment status and returns Copy to clipboardcaptured once it's paid — which creates the Payment record, captures it, and flips the order to paid/captured. If the QR expires or the payment fails, Copy to clipboardpayment.expired / Copy to clipboardpayment.failed are mapped to a Copy to clipboardFAILED action — which Medusa 2.18's webhook processor ignores, so the order remains awaiting and no capture occurs. The storefront must surface CutLuy's own payment status (poll Copy to clipboardgetPaymentStatus), and operators see a warning log per event.

Async flow requirement: Copy to clipboardauthorizePayment polls Copy to clipboardGET /v1/payments/:id on every call and only returns Copy to clipboardpending_authorization while the payment is still pending. This is what lets the webhook-driven autocapture flow create and capture the Payment.

Requirements

  • Medusa v2.17.0 or later (uses the async payment methods support)
  • Node.js 20+
  • A CutLuy account with an API key and a configured webhook endpoint

Testing

A complete end-to-end test harness lives in the Copy to clipboardmedusa-test-app/ directory (sibling of this repo — not part of the package):

  • Copy to clipboarddocker-compose.yml — Postgres + Redis for the Medusa app
  • Copy to clipboardmedusa-app/ — a Copy to clipboardcreate-medusa-app backend with the plugin registered
  • Copy to clipboardcutluy-mock/server.mjs — a mock of the CutLuy API (Copy to clipboardhttp://localhost:8080/v1), so no real credentials are needed
  • Copy to clipboarde2e-test.mjs — drives the full flow via the API: cart → payment session (Copy to clipboardpp_cutluy_cutluy) → verify Copy to clipboardqr_string/Copy to clipboardcheckout_url → complete cart (order "awaiting") → simulate customer paying at the mock → deliver a signed Copy to clipboardpayment.completed webhook → assert the order becomes Copy to clipboardcaptured

When you later point the provider at the real CutLuy API (Copy to clipboardCUTLUY_API_URL + real Copy to clipboardck_... key), the same script works unchanged.

Dependency & security posture: the published package ships zero runtime dependencies — it only declares the Copy to clipboard@medusajs/framework peer (>=2.17.0) that any Medusa app already provides. Scanner findings (CVEs, telemetry, minified files) that appear for this package come from Medusa core's own dependency tree (e.g. Copy to clipboard@medusajs/telemetry, which is opt-out via Copy to clipboardMEDUSA_TELEMETRY_DISABLED=true), and are identical for every Medusa plugin — they resolve upstream when Medusa updates its dependencies.

1. Install

Install locally for development (yalc)

From this plugin's directory (pnpm is the package manager for this repo):

pnpm install
pnpm medusa plugin:publish # pushes to the LOCAL yalc registry (dev only — this is not npm)

Then, in your Medusa application:

npx medusa plugin:add medusa-payment-cutluy

While developing, run Copy to clipboardpnpm medusa plugin:develop in this plugin's directory to watch changes and auto-update the app.

Install from npm

npm install medusa-payment-cutluy

2. Configure

In Copy to clipboardmedusa-config.ts of your Medusa application, register the provider in the Payment Module's Copy to clipboardproviders array:

import { defineConfig } from "@medusajs/framework/utils"
module.exports = defineConfig({
// ...other config
modules: [
{
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{
// provider installed from the local registry or npm
resolve: "medusa-payment-cutluy/providers/cutluy",
id: "cutluy",
options: {
apiKey: process.env.CUTLUY_API_KEY,
webhookSecret: process.env.CUTLUY_WEBHOOK_SECRET,
// apiUrl: "https://cutluy.com/v1", // optional override
// timeoutMs: 15000, // optional
},
},

Add the environment variables to your application's Copy to clipboard.env:

# apps/backend/.env
CUTLUY_API_KEY=ck_live_...
CUTLUY_WEBHOOK_SECRET=whsec_...
The provider's identifier is Copy to clipboardpp_cutluy_cutluy. Enable it in a region from the Medusa Admin (Settings → Regions → Payment Providers).

Options

Option Required Description Copy to clipboardapiKey ✅ CutLuy secret API key (Copy to clipboardck_live_... / Copy to clipboardck_test_...) Copy to clipboardwebhookSecret ⚠️ Signing secret used to verify Copy to clipboardX-CutLuy-Signature. Without it webhooks are rejected. Copy to clipboardapiUrl Override the API base URL (default Copy to clipboardhttps://cutluy.com/v1) Copy to clipboardtimeoutMs HTTP request timeout (default Copy to clipboard15000)

3. Configure the webhook in CutLuy

  1. Make sure your CutLuy store has a payment link configured (payment creation returns Copy to clipboard404 payment_link_not_found otherwise).
  2. In the CutLuy dashboard, go to Webhooks.
  3. Add an endpoint pointing at Medusa's built-in payment webhook listener: (Copy to clipboardcutluy is the provider's Copy to clipboardidentifier, repeated for the provider Copy to clipboardid.)
https://<your-medusa-backend>/hooks/payment/cutluy_cutluy
  1. Copy the endpoint's signing secret into Copy to clipboardCUTLUY_WEBHOOK_SECRET.

The provider verifies the Copy to clipboardX-CutLuy-Signature header (HMAC-SHA256 of Copy to clipboard<t>.<rawBody>) before trusting any event. Medusa's built-in listener acks the request with 200 immediately and processes the event asynchronously (~5s delay, up to 3 internal attempts). Invalid or missing signatures are logged and dropped — the request is still acked, so CutLuy does not retry after a 2xx (non-2xx or timeout responses are retried with exponential backoff, up to 8 times); monitor your backend logs for signature failures. Use the dashboard's Send test or resend a delivery to exercise your endpoint.

Before completing, the provider also verifies the webhook's payment against its payment session: the session must exist, the payment must be USD, and the webhook amount must match the session amount cent-exact. On a mismatch or unknown session the event is logged and ignored (the order stays Copy to clipboardawaiting). The payload is HMAC-authenticated, so this guards against CutLuy-side drift, not forgery.

Payment sweep job

Because Medusa 2.18's webhook processor ignores Copy to clipboardpayment.expired / Copy to clipboardpayment.failed events, an order whose QR expired or whose payment failed would stay Copy to clipboardawaiting forever with no signal. The plugin ships a scheduled job, Copy to clipboardpoll-cutluy-payments, that closes the loop:

  • Runs every 15 minutes (node-schedule cron Copy to clipboard*/15 * * * *).
  • Lists the provider's payment sessions older than 15 minutes that are still Copy to clipboardpending / Copy to clipboardpending_authorization, polls CutLuy for each, and for payments that are Copy to clipboardexpired or Copy to clipboardfailed at CutLuy:
    • logs a warning with the session id, CutLuy payment id, amount, and currency, and
    • marks the session Copy to clipboarderror (best-effort) so it stops being silently pending.
  • Sessions are only touched once (they leave the pending set), so re-runs are idempotent.

To load the job, the plugin must be listed in the app's Copy to clipboardplugins array (the provider itself is registered under Copy to clipboardmodules):

plugins: ["medusa-payment-cutluy"],

The staleness window is tunable via Copy to clipboardCUTLUY_SWEEP_STALE_AFTER_MS (default Copy to clipboard900000).

4. Storefront integration

The payment session Copy to clipboarddata contains everything the storefront needs:

{
"id": "PUETcMUOKStjZsCb6zAl8kg9fMRGM85x",
"status": "pending",
"amount": "1.50",
"currency": "USD",
"qr_string": "00020101021229...", // render as a QR code
"checkout_url": "https://cutluy.com/pay/PUETcMUOKStjZsCb6zAl8kg9fMRGM85x",
"expires_at": "2026-07-09T12:05:00.000Z"
}

Choose one of:

  • Redirect the customer to Copy to clipboardcheckout_url (hosted, branded page with countdown and live status), or
  • Render Copy to clipboardqr_string as a QR code in your own UI (e.g. with a Copy to clipboardqrcode library) and poll the cart/order status.

After the customer pays, the Copy to clipboardpayment.completed webhook completes the order automatically — no storefront polling required.

CutLuy's hosted checkout redirects back to your configured success/failure URLs after a terminal payment, appending Copy to clipboard?status=success|failed&payment_id=…&reference_id=…. Since Medusa drops Copy to clipboardpayment.expired / Copy to clipboardpayment.failed webhooks, the Copy to clipboardfailed redirect (or polling Copy to clipboardgetPaymentStatus) is how the storefront learns the order failed — the order itself stays Copy to clipboardawaiting until the sweep job or an operator acts.

5. Development

pnpm install # install dependencies
pnpm test # run unit tests (vitest)
pnpm build # medusa plugin:build → outputs to .medusa/server
pnpm dev # watch + push to the local yalc registry for the test app

6. Testing in a full Medusa app (Docker)

This repo ships only the plugin. To test it end-to-end, run a Medusa app in Docker and install the plugin into it. Follow the official guide — Install Medusa with Docker — then:

  1. Clone the DTC Starter repo and set up Copy to clipboarddocker-compose.yml, Copy to clipboardDockerfile, Copy to clipboardstart.sh as described in the guide.
  2. Install the plugin locally via yalc (Copy to clipboardnpx medusa plugin:publish here, then Copy to clipboardnpx yalc add medusa-payment-cutluy in the app), or mount this plugin's folder and install it with Copy to clipboardmedusa plugin:add.
  3. Register the provider in Copy to clipboardapps/backend/medusa-config.ts (see Configure) and add the Copy to clipboardCUTLUY_* env vars to Copy to clipboardapps/backend/.env.
  4. Copy to clipboarddocker compose up --build -d, create an admin user, and enable the CutLuy payment provider in a region.
  5. Expose the backend to the internet (e.g. with a tunnel) and set the webhook URL in the CutLuy dashboard.

API mapping

Medusa provider method CutLuy API call Copy to clipboardinitiatePayment Copy to clipboardPOST /v1/payments Copy to clipboardgetPaymentStatus Copy to clipboardGET /v1/payments/:id Copy to clipboardretrievePayment Copy to clipboardGET /v1/payments/:id Copy to clipboardgetWebhookActionAndData webhook events (signature verified) Copy to clipboardcapturePayment / Copy to clipboardcancelPayment / Copy to clipboarddeletePayment / Copy to clipboardupdatePayment no-op (not in CutLuy v1 API) Copy to clipboardrefundPayment throws — not supported by CutLuy yet

Status mapping: Copy to clipboardpending/Copy to clipboardscanned → pending · Copy to clipboardpaid → captured · Copy to clipboardexpired/Copy to clipboardfailed → error/failed.

License

MIT

You may also like

Browse all integrations

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?