medusa-plugin-quickpay
v1.0.0
Published
Medusa v2 QuickPay payment provider for hosted-checkout payments, refunds, and webhook reconciliation
Downloads
28
Maintainers
Readme
medusa-plugin-quickpay
A QuickPay payment provider for Medusa v2. One QuickPay account, three storefront payment options — credit card, MobilePay, and Anyday — all via QuickPay's hosted checkout window.
- Full lifecycle: initiate, authorize, capture, refund, cancel
- HMAC-SHA256 webhook verification with automatic key rotation
- Idempotent webhook recording (safe under concurrent delivery)
- Admin order widget: status, refunds, regenerate link, chargebacks
- Currency-aware: DKK-only methods auto-hidden on non-DKK carts
Requirements
- Medusa
>= 2.13.5 - Node.js
>= 20 - A QuickPay merchant account
Install
pnpm add medusa-plugin-quickpay
# or: npm install medusa-plugin-quickpaySetup
1. Add your QuickPay API key to your backend .env
(QuickPay Manager → Settings → Users → your API user → copy the API key):
QUICKPAY_API_KEY=your_api_key_here
# Public backend URL — required so QuickPay can reach your webhook.
# Use an ngrok tunnel in development, your real domain in production.
BACKEND_PUBLIC_URL=https://api.yourdomain.com2. Register the provider and plugin in medusa-config.ts:
import { Modules, defineConfig } from "@medusajs/framework/utils"
export default defineConfig({
modules: [
{
key: Modules.PAYMENT,
resolve: "@medusajs/payment",
options: {
providers: [
{
resolve: "medusa-plugin-quickpay/providers/quickpay",
id: "quickpay",
options: {
apiKey: process.env.QUICKPAY_API_KEY,
},
},
],
},
},
],
plugins: [{ resolve: "medusa-plugin-quickpay" }],
})3. Enable the providers in Medusa Admin → Settings → Regions → your region:
| Provider ID | Method | Currencies |
|-------------|--------|------------|
| pp_creditcard_quickpay | Credit card | All |
| pp_mobilepay_quickpay | MobilePay | DKK only |
| pp_anyday_quickpay | Anyday (pay in installments) | DKK only |
Each customer lands on a hosted window where their chosen method is primary but they can still switch to another without abandoning the cart.
That's it — your storefront now shows the enabled QuickPay methods at checkout.
Configuration
Pass these in the provider options. Only apiKey is required.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | required | QuickPay API key. |
| callbackUrl | URL | ${BACKEND_PUBLIC_URL}/quickpay/webhook | Webhook endpoint. |
| continueUrl | URL | ${STORE_CORS[0]}/checkout/quickpay/return | Redirect after success. |
| cancelUrl | URL | ${STORE_CORS[0]}/checkout/quickpay/cancel | Redirect after cancel. |
| baseUrl | URL | https://api.quickpay.net | QuickPay API base. Override only for proxies. |
| acceptVersion | "v10" | "v11b" | "v10" | Accept-Version header. |
| defaultPaymentMethods | string | creditcard,mobilepay,anyday-split | Hosted-window method tokens. |
| defaultLanguage | string | — | ISO 639-1 code (e.g. da, en). |
| brandingId | number | — | QuickPay branding ID for the hosted window. |
| autoCapture | boolean | false | Capture immediately after authorize. |
| syncRefunds | boolean | true | Use QuickPay's ?synchronized refund mode. |
| testMode | "enforce_true" | "enforce_false" | "any" | enforce_false in prod, else any | Webhook test-mode guard. |
The webhook private key is not configured manually — the plugin fetches it from QuickPay and caches it, refreshing automatically on key rotation.
Storefront integration
The hosted-window flow needs three small additions to your storefront.
1. Payment button — redirect to the hosted-window URL from the session:
const isQuickPay = (id?: string) => id?.endsWith("_quickpay")
const QuickPayPaymentButton = ({ cart, notReady }: PaymentButtonProps) => {
const session = cart.payment_collection?.payment_sessions?.find((s) =>
isQuickPay(s.provider_id)
)
const linkUrl = (session?.data as any)?.quickpay_link_url as string | undefined
return (
<Button disabled={notReady || !linkUrl} onClick={() => (window.location.href = linkUrl!)}>
Continue to QuickPay
</Button>
)
}2. Hide DKK-only methods on non-DKK carts in your payment selector:
export const isPaymentMethodAvailableForCurrency = (
providerId: string,
currencyCode?: string | null
): boolean => {
const dkkOnly = ["pp_mobilepay_quickpay", "pp_anyday_quickpay"]
if (!currencyCode || !dkkOnly.includes(providerId)) return true
return currencyCode.toUpperCase() === "DKK"
}3. Return & cancel pages under app/[countryCode]/(checkout)/checkout/quickpay/:
return/— QuickPay redirects here withpayment_idandpsin the URL. PollGET /quickpay/return?payment_id={id}&ps={ps}(forward both —psis required) and call Medusa's place-order flow when the outcome iscompletable.cancel/— callPOST /quickpay/cancelwith{ payment_id, ps }to void the pending authorize and clean up, then return the customer to the cart.
API endpoints
| Endpoint | Purpose |
|----------|---------|
| POST /quickpay/webhook | QuickPay callbacks. HMAC-SHA256 verified; returns 401 on bad signature. |
| GET /quickpay/return | Post-payment validation. Requires payment_id + matching ps. |
| POST /quickpay/cancel | Customer cancel cleanup. Requires payment_id + matching ps. |
| POST /admin/quickpay/payments/:id/regenerate-link | Admin — fresh hosted-window URL (pre-authorization only). |
/quickpay/returnand/quickpay/cancelrequireps(the QuickPay order id) to match the payment — this binds each request to a specific payment and stops the endpoints from being used to probe arbitrary payment ids.
Testing it locally
- In QuickPay Manager, enable Allow test transactions and activate at least one acquirer (e.g. Clearhaus test).
- Expose your backend:
ngrok http 9000, and setBACKEND_PUBLIC_URLto the tunnel URL. - Pay with QuickPay's test card:
1000 0000 0000 0008, any future expiry, any CVV.
Troubleshooting
| Symptom | Fix |
|---------|-----|
| Webhook callback Failed / no status in QuickPay | BACKEND_PUBLIC_URL isn't publicly reachable. Set it to a real/ngrok URL and restart. |
| MobilePay / Anyday missing at checkout | They only show for DKK carts. Check the region currency, that the provider is enabled, and that the storefront filter is in place. |
| Webhook signature fails (401) | The key refreshes automatically on rotation. Ensure nothing strips the raw request body before the webhook route. |
| Payment stuck in pending | Confirm the webhook was delivered (QuickPay dashboard → payment → operations) and the backend is reachable. |
| Cannot find module '@medusajs/utils' | Run pnpm add @medusajs/utils in your backend project. |
Programmatic exports
For advanced use the package exports its provider class and pure helpers
(QuickPayClient, verifyQuickPaySignature, validateReturnOutcome,
derivePaymentTotals, resolveQuickPayOptions, and more) from the package root:
import { QuickPayClient, verifyQuickPaySignature } from "medusa-plugin-quickpay"Development
pnpm install
pnpm build # compile (medusa plugin:build)
pnpm dev # watch mode
pnpm test # run the test suite