npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

medusa-payment-cybersource

v1.1.1

Published

CyberSource payment provider for Medusa.js v2 with Flex Microform (PCI DSS SAQ-A)

Readme

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 (@medusajs/medusa >= 2.0.0) | | Node.js | 18+ | | CyberSource | Business Center account |

Installation

npm install medusa-payment-cybersource

Environment Variables

Add these to your .env:

CYBERSOURCE_MERCHANT_ID=your_merchant_id
CYBERSOURCE_KEY_ID=your_shared_key_id
CYBERSOURCE_SECRET_KEY=your_shared_secret_key
CYBERSOURCE_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 medusa-config.ts, add the plugin to both plugins (for the API route) and modules (for the payment provider):

import { 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",
            options: {
              merchantID: process.env.CYBERSOURCE_MERCHANT_ID,
              merchantKeyId: process.env.CYBERSOURCE_KEY_ID,
              merchantsecretKey: process.env.CYBERSOURCE_SECRET_KEY,
              environment:
                process.env.CYBERSOURCE_ENV === "production"
                  ? "production"
                  : "sandbox",
              capture: process.env.CYBERSOURCE_AUTO_CAPTURE === "true",
            },
          },
        ],
      },
    },
  ],
})

Plugin Options

| Option | Type | Description | |--------|------|-------------| | merchantID | string | Required. Your CyberSource Merchant ID | | merchantKeyId | string | Required. Shared key ID (HTTP Signature) | | merchantsecretKey | string | Required. Shared secret key (HTTP Signature) | | environment | string | "sandbox" or "production". Default: "sandbox" | | capture | boolean | Auto-capture on authorization (sale mode). Default: false | | allowedCardNetworks | string[] | Card networks for Flex. Default: ["VISA","MASTERCARD","AMEX","DISCOVER"] |

Payment Flow

Storefront          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-->|
    |                  |  + deviceInformation |
    |                  |<-- AUTHORIZED --------|
    |<-- { cs_payment_id } -------------|
    |                  |                    |
    | POST /store/carts/:id/complete   |
    |----------------->|                    |
    |                  | authorizePayment() |
    |                  | reads cs_payment_id|
    |<-- order created--|                    |

Frontend Integration

1. Load Flex Microform

<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.

const 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 = true
document.head.appendChild(script)

// org_id values:
// Sandbox:    1snn5n9w
// Production: k8vif92e  (confirm in Business Center → Decision Manager)

3. Initialize Flex

// captureContext comes from payment_session.data.captureContext
const 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 placeOrder():

async function authorizePayment(paymentSessionId, fingerprintSessionId) {
  // 1. Get transient token from Flex Microform
  const 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 route
  const 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
      bill_to: {               // optional but recommended
        firstName: "John",
        lastName: "Doe",
        email: "[email protected]",
        address1: "123 Main St",
        locality: "Guatemala City",
        administrativeArea: "Guatemala",
        postalCode: "01001",
        country: "GT",
      },
    }),
  })

  const result = await response.json()

  if (!response.ok) {
    throw new Error(result.message || "Payment declined")
  }

  return result // { success: true, cs_payment_id, cs_status }
}

// Usage
await authorizePayment(
  cart.payment_collection.payment_sessions[0].id,
  fingerprintSessionId,
)
await placeOrder() // Medusa completes the order using the stored cs_payment_id

/store/cybersource/authorize — Request / Response

Request body:

| Field | Required | Description | |-------|----------|-------------| | payment_session_id | Yes | Medusa payment session ID | | transient_token | Yes | JWT from microform.createToken() | | fingerprint_session_id | No | Session ID used in the ThreatMetrix script URL | | bill_to | No | Billing address object for AVS/fraud checks |

Success response (200):

{ "success": true, "cs_payment_id": "7278957202756800104005", "cs_status": "AUTHORIZED" }

Declined response (402):

{ "error": "Payment declined", "reason": "INSUFFICIENT_FUND", "cs_status": "DECLINED" }

Device Fingerprint

The plugin sends deviceInformation.fingerprintSessionId and useRawFingerprintSessionId: true to the CyberSource Payments API. The useRawFingerprintSessionId flag is required so that CyberSource looks up ThreatMetrix using the raw session ID (your UUID) instead of the default {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 CYBERSOURCE_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 (order.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 | |-----------|-------| | cs_last_refund_id is set | 🔵 REFUNDED | | cs_status = AUTHORIZED + cs_capture_id set (auto-capture) | 🟢 CAPTURED | | cs_status = AUTHORIZED (no capture ID) | 🟠 AUTHORIZED | | cs_status = CAPTURED | 🟢 CAPTURED | | cs_status = VOIDED | ⚫ VOIDED | | cs_status = DECLINED | 🔴 DECLINED |

Admin Refund Route

Medusa's default refund UI has a pendingDifference validation that can block refunds in some edge cases. Add this route to your Medusa store for a direct refund bypass:

Create src/api/admin/cybersource/refund/route.ts in your Medusa project:

import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

type RefundRequestBody = {
  payment_id: string
  amount?: number
  note?: string
}

export const POST = async (
  req: MedusaRequest<RefundRequestBody>,
  res: MedusaResponse
) => {
  const { payment_id, amount, note } = req.body

  if (!payment_id) {
    return res.status(400).json({ error: "payment_id is required" })
  }

  const paymentModule = req.scope.resolve(Modules.PAYMENT)

  const payments = await paymentModule.listPayments(
    { id: [payment_id] },
    { relations: ["captures", "refunds"] }
  )
  const payment = payments[0]

  if (!payment) {
    return res.status(404).json({ error: "Payment not found" })
  }

  const captured = (payment.captures ?? []).reduce(
    (sum: number, c: any) => sum + Number(c.amount ?? 0),
    0
  )
  const alreadyRefunded = (payment.refunds ?? []).reduce(
    (sum: number, r: any) => sum + Number(r.amount ?? 0),
    0
  )
  const refundable = captured - alreadyRefunded

  if (refundable <= 0) {
    return res.status(400).json({ error: "No capturable amount available to refund" })
  }

  const refundAmount = amount ?? refundable

  if (refundAmount > refundable) {
    return res.status(400).json({
      error: `Cannot refund ${refundAmount}. Maximum refundable: ${refundable}`,
    })
  }

  const updatedPayment = await paymentModule.refundPayment({
    payment_id,
    amount: refundAmount,
    created_by: (req as any).auth_context?.actor_id,
    note,
  })

  return res.json({ payment: updatedPayment })
}

Call it from your admin UI or custom dashboard:

curl -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

# Clone
git clone https://github.com/Eleven-Estudio/medusa-payment-cybersource.git
cd medusa-payment-cybersource

# Install dependencies
npm install

# Build
npm run build

# Watch mode (backend only)
npm run dev

Linking to a local Medusa store with yalc

# In the plugin directory
npm run build
npx yalc push

# In your Medusa store directory
npx yalc add medusa-payment-cybersource
npx medusa develop

After any plugin change, re-run npm run build && npx yalc push in the plugin directory, then fully restart the Medusa server (yalc updates node_modules, hot-reload won't pick it up).

CyberSource Resources

License

MIT