Cybersource
Processes online payments and fraud management
Medusa Payment Cybersource for Medusa
CyberSource payment plugin for Medusa.js v2, built on Flex Microform v2 (PCI DSS SAQ-A compliant).
Features
- Flex Microform v2 — card data is tokenized directly at CyberSource; it never touches your server
- Device Fingerprint (ThreatMetrix) — fraud scoring via device profiling before payment
- Authorization + manual capture from the Medusa admin dashboard
- Auto-capture (sale mode) for instant settlement
- Partial and full refunds
- Zero-amount orders — 100% discounts and free orders are handled automatically (no gateway call)
- Admin widget — CyberSource transaction details panel injected into order detail view
Requirements
Medusa.js v2 (Copy to clipboard@medusajs/medusa >= 2.0.0) Node.js 18+ CyberSource Business Center account
Installation
1npm install medusa-payment-cybersource
Environment Variables
Add these to your Copy to clipboard.env:
12345CYBERSOURCE_MERCHANT_ID=your_merchant_idCYBERSOURCE_KEY_ID=your_shared_key_idCYBERSOURCE_SECRET_KEY=your_shared_secret_keyCYBERSOURCE_ENV=sandbox # or "production"CYBERSOURCE_AUTO_CAPTURE=false # set to "true" for sale/auto-capture mode
Where to find these values: CyberSource Business Center → Account Management → Transaction Security Keys → Security Keys for the HTTP Signature Security Policy
Configuration
In Copy to clipboardmedusa-config.ts, add the plugin to both Copy to clipboardplugins (for the API route) and Copy to clipboardmodules (for the payment provider):
1234567891011121314151617181920import { loadEnv, defineConfig } from "@medusajs/framework/utils"loadEnv(process.env.NODE_ENV || "development", process.cwd())module.exports = defineConfig({plugins: [// Required: registers the built-in /store/cybersource/authorize route{ resolve: "medusa-payment-cybersource" },],projectConfig: {// ... your existing config},modules: [{resolve: "@medusajs/medusa/payment",options: {providers: [{resolve: "medusa-payment-cybersource",id: "cybersource",
Plugin Options
Option Type Description Copy to clipboardmerchantID Copy to clipboardstring Required. Your CyberSource Merchant ID Copy to clipboardmerchantKeyId Copy to clipboardstring Required. Shared key ID (HTTP Signature) Copy to clipboardmerchantsecretKey Copy to clipboardstring Required. Shared secret key (HTTP Signature) Copy to clipboardenvironment Copy to clipboardstring Copy to clipboard"sandbox" or Copy to clipboard"production". Default: Copy to clipboard"sandbox" Copy to clipboardcapture Copy to clipboardboolean Auto-capture on authorization (sale mode). Default: Copy to clipboardfalse Copy to clipboardallowedCardNetworks Copy to clipboardstring[] Card networks for Flex. Default: Copy to clipboard["VISA","MASTERCARD","AMEX","DISCOVER"]
Payment Flow
1234567891011121314151617181920Storefront Backend CyberSource| | || select method | ||----------------->| || |-- /flex/v2/sessions-->||<-- captureContext JWT ----------------|| | || Flex iFrames render (SAQ-A) || Fingerprint script loads in bg || | || microform.createToken() ||----------------------------------------->||<-- transient_token (JWT 15 min) ---------|| | || POST /store/cybersource/authorize || { payment_session_id, || transient_token, || fingerprint_session_id } ||----------------->| || |-- /pts/v2/payments-->|
Frontend Integration
1. Load Flex Microform
1<script src="https://flex.cybersource.com/microform/bundle/v2/flex-microform.min.js"></script>
2. Load Device Fingerprint Script
When the user selects CyberSource as payment method, generate a unique session ID and load the ThreatMetrix script. The same ID must be sent in the authorize request.
12345678910const fingerprintSessionId = crypto.randomUUID()const script = document.createElement("script")script.src = `https://h.online-metrix.net/fp/tags.js?org_id=${ORG_ID}&session_id=${fingerprintSessionId}`script.async = truedocument.head.appendChild(script)// org_id values:// Sandbox: 1snn5n9w// Production: k8vif92e (confirm in Business Center → Decision Manager)
3. Initialize Flex
123456789// captureContext comes from payment_session.data.captureContextconst flex = new Flex(captureContext)const microform = flex.microform()const cardNumber = microform.createField("number", { placeholder: "Card number" })const cvn = microform.createField("securityCode", { placeholder: "CVV" })cardNumber.load("#card-number-container")cvn.load("#cvn-container")
4. Tokenize and Pre-authorize
Call this before Copy to clipboardplaceOrder():
1234567891011121314151617181920async function authorizePayment(paymentSessionId, fingerprintSessionId) {// 1. Get transient token from Flex Microformconst transientToken = await new Promise((resolve, reject) => {microform.createToken({ expirationMonth: "12", expirationYear: "2030" }, (err, token) => {if (err) reject(err)else resolve(token)})})// 2. Pre-authorize at CyberSource via the built-in plugin routeconst response = await fetch("/store/cybersource/authorize", {method: "POST",headers: {"Content-Type": "application/json","x-publishable-api-key": YOUR_PUBLISHABLE_API_KEY,},body: JSON.stringify({payment_session_id: paymentSessionId,transient_token: transientToken,fingerprint_session_id: fingerprintSessionId, // same UUID used in the script URL
Copy to clipboard/store/cybersource/authorize — Request / Response
Request body:
Field Required Description Copy to clipboardpayment_session_id Yes Medusa payment session ID Copy to clipboardtransient_token Yes JWT from Copy to clipboardmicroform.createToken() Copy to clipboardfingerprint_session_id No Session ID used in the ThreatMetrix script URL Copy to clipboardbill_to No Billing address object for AVS/fraud checks
Success response (200):
1{ "success": true, "cs_payment_id": "7278957202756800104005", "cs_status": "AUTHORIZED" }
Declined response (402):
1{ "error": "Payment declined", "reason": "INSUFFICIENT_FUND", "cs_status": "DECLINED" }
Device Fingerprint
The plugin sends Copy to clipboarddeviceInformation.fingerprintSessionId and Copy to clipboarduseRawFingerprintSessionId: true to the CyberSource Payments API. The Copy to clipboarduseRawFingerprintSessionId flag is required so that CyberSource looks up ThreatMetrix using the raw session ID (your UUID) instead of the default Copy to clipboard{merchantId}{sessionId} composite key — which would cause a mismatch with what the frontend script registered.
You can verify fingerprint collection is working by checking a transaction in CyberSource Business Center. A successful integration shows a hash under Device Fingerprint ID instead of "Not Submitted".
Capture Modes
Manual Capture (default)
CyberSource authorizes the card on order placement. You capture the funds from the Medusa Admin → Orders → Payment panel. Authorization expires in 5–7 days if not captured.
Auto-Capture (sale mode)
Set Copy to clipboardCYBERSOURCE_AUTO_CAPTURE=true. CyberSource processes authorization and capture together; the payment is marked as captured immediately on order placement.
Admin Widget
The plugin injects a CyberSource panel into the order detail page of the Medusa admin (Copy to clipboardorder.details.side.after). It shows:
Field Description Status badge Derived display status (see table below) Transaction ID CyberSource authorization ID Capture ID Only shown if different from Transaction ID (manual capture) Reconciliation ID CyberSource reconciliation ID Card Card brand + last 4 digits Last Refund ID ID of the last refund issued Last Refund Amount Amount of the last refund
Status badge logic:
Condition Badge Copy to clipboardcs_last_refund_id is set 🔵 REFUNDED Copy to clipboardcs_status = AUTHORIZED + Copy to clipboardcs_capture_id set (auto-capture) 🟢 CAPTURED Copy to clipboardcs_status = AUTHORIZED (no capture ID) 🟠 AUTHORIZED Copy to clipboardcs_status = CAPTURED 🟢 CAPTURED Copy to clipboardcs_status = VOIDED ⚫ VOIDED Copy to clipboardcs_status = DECLINED 🔴 DECLINED
Admin Refund Route
Medusa's default refund UI has a Copy to clipboardpendingDifference validation that can block refunds in some edge cases. Add this route to your Medusa store for a direct refund bypass:
Create Copy to clipboardsrc/api/admin/cybersource/refund/route.ts in your Medusa project:
1234567891011121314151617181920import { MedusaRequest, MedusaResponse } from "@medusajs/framework"import { Modules } from "@medusajs/framework/utils"type RefundRequestBody = {payment_id: stringamount?: numbernote?: string}export const POST = async (req: MedusaRequest<RefundRequestBody>,res: MedusaResponse) => {const { payment_id, amount, note } = req.bodyif (!payment_id) {return res.status(400).json({ error: "payment_id is required" })}const paymentModule = req.scope.resolve(Modules.PAYMENT)
Call it from your admin UI or custom dashboard:
1234curl -X POST http://localhost:9000/admin/cybersource/refund \-H "Authorization: Bearer <admin_token>" \-H "Content-Type: application/json" \-d '{ "payment_id": "pay_01...", "amount": 50.00 }'
Development
123456789101112# Clonegit clone https://github.com/Eleven-Estudio/medusa-payment-cybersource.gitcd medusa-payment-cybersource# Install dependenciesnpm install# Buildnpm run build# Watch mode (backend only)npm run dev
Linking to a local Medusa store with yalc
1234567# In the plugin directorynpm run buildnpx yalc push# In your Medusa store directorynpx yalc add medusa-payment-cybersourcenpx medusa develop
After any plugin change, re-run Copy to clipboardnpm run build && npx yalc push in the plugin directory, then fully restart the Medusa server (yalc updates Copy to clipboardnode_modules, hot-reload won't pick it up).
CyberSource Resources
- Business Center — sandbox + production portal
- Flex Microform v2 docs
- Payments API reference
- Sandbox test cards
License
MIT

