

Medusa
The Cryptonly payment provider for Medusa lets your Medusa v2 store accept cryptocurrency payments through Cryptonly’s hosted checkout.
Customers select Cryptonly at checkout, pay on the Cryptonly payment page, and return to your store. The Medusa payment status updates automatically via signed webhooks — no manual reconciliation.
Setting up takes four steps:
- Install and register the provider
- Configure your credentials and webhook URL
- Enable the provider on a region in Medusa Admin
- Wire up your storefront
How the payment flow works
Section titled “How the payment flow works”At a high level:
- The customer checks out and chooses Cryptonly as the payment method.
- Medusa creates a Cryptonly invoice server-side — your API key never reaches the browser.
- The storefront redirects the customer to the hosted payment page (
paymentPageUrl). - After payment, Cryptonly sends signed webhooks to your Medusa backend.
- The provider verifies each webhook and marks the payment session captured once the invoice is
paidoroverpaid.
For details on invoices and statuses, see Invoices.
Requirements
Section titled “Requirements”| Requirement | Details |
|---|---|
| Medusa | v2.8+ (peer dependency) |
| Node | 20+ |
| Cryptonly account | merchant.cryptonly.net |
| Public HTTPS backend | Cryptonly must reach {MEDUSA_BACKEND_URL}/hooks/payment/cryptonly_cryptonly |
Installation
Section titled “Installation”Install the plugin:
npm install @cryptonly/medusa-plugin-cryptonlyRegister the provider in medusa-config.ts:
module.exports = defineConfig({ modules: [ { resolve: "@medusajs/medusa/payment", options: { providers: [ { resolve: "@cryptonly/medusa-plugin-cryptonly/providers/cryptonly", id: "cryptonly", options: { apiKey: process.env.CRYPTONLY_API_KEY, webhookSigningKey: process.env.CRYPTONLY_WEBHOOK_SIGNING_KEY, sandbox: process.env.CRYPTONLY_SANDBOX === "true", backendUrl: process.env.MEDUSA_BACKEND_URL, }, }, ], }, }, ],})Finally, enable the provider on a region in Medusa Admin.
Configuration
Section titled “Configuration”| Option | Required | Description |
|---|---|---|
apiKey | Yes | API key from the account Security settings. The key selects the account. See required scopes. |
webhookSigningKey | Yes | Signing key from the same account. Verifies incoming Cryptonly webhooks |
backendUrl | Yes | Public HTTPS Medusa origin used to build the webhook URL |
webhookUrl | Yes | Full HTTPS webhook URL (overrides backendUrl) |
sandbox | No | Use the Cryptonly sandbox API (default: false) |
returnUrl | No | Default browser URL after cancelled / expired |
successUrl | No | Default browser URL after paid / overpaid |
failedUrl | No | Default browser URL after failed / suspended / partially_paid |
expiresInMinutes | No | Invoice expiry passed to Cryptonly (default: 1440) |
Provide either backendUrl or webhookUrl.
API key scopes
Section titled “API key scopes”Create the key on the account’s Security settings and enable only these scopes:
| Scope | Used for |
|---|---|
currencies.list | Confirm the API key can reach the merchant API |
invoices.create | Start checkout (POST /invoice) and recreate an invoice when the cart amount or currency changes |
invoices.list | Read the invoice while authorizing, polling status, or retrieving the session (GET /invoice) |
invoices.cancel | Cancel the invoice when the customer abandons checkout or the cart total changes (POST /invoice/cancel) |
Keep in mind:
apiKeyandwebhookSigningKeymust be non-empty — otherwise the provider will not register.- Webhook URLs must be public HTTPS. Localhost and private IPs are rejected.
- The provider sets
webhookUrlautomatically on every invoice it creates, so you do not need to configure a default invoice webhook in the Cryptonly dashboard.
Browser return URLs
Section titled “Browser return URLs”Cryptonly’s hosted page shows a “Back to Store” button whose destination depends on the invoice status:
| Option | Used when invoice status is |
|---|---|
successUrl | paid / overpaid |
returnUrl | cancelled / expired |
failedUrl | failed / suspended / partially_paid |
A few things to know:
- Any
httporhttpsURL is accepted, including local storefronts such ashttp://localhost:8000/.... - Provider options are only fallbacks. Prefer passing per-session values in the
initiatePaymentSessiondataso each cart gets the right confirmation or retry URL.
Recommended storefront mapping:
successUrl→ confirmation / poll page (e.g./order/cryptonly/return?cart_id=…)returnUrl/failedUrl→ checkout review, so the customer can try again (e.g./checkout?step=review&cart_id=…)
Include cart_id on all three URLs. On the Next.js starter, set the _medusa_cart_id cookie to SameSite=Lax so it is sent on the top-level return from Cryptonly — otherwise checkout may 404 after a cancel.
For webhook signing details, see Webhooks. For sandbox testing (simulated payments, test webhooks), see Testing your integration.
Storefront
Section titled “Storefront”Start a new session on every pay attempt
Section titled “Start a new session on every pay attempt”On each “Pay with Cryptonly” click, call initiatePaymentSession again and redirect to the new paymentPageUrl. Do not reuse a URL from an earlier session — once the customer cancels or the invoice expires on Cryptonly, that URL is dead. (Same principle as Stripe Checkout: one session per pay attempt.)
const base = baseUrl.replace(/\/$/, "")const successUrl = `${base}/order/cryptonly/return?cart_id=${cart.id}`const returnUrl = `${base}/checkout?step=review&cart_id=${cart.id}`const failedUrl = `${base}/checkout?step=review&cart_id=${cart.id}`
const { payment_collection } = await initiatePaymentSession(cart, { provider_id: "pp_cryptonly_cryptonly", data: { returnUrl, successUrl, failedUrl },})
const session = payment_collection.payment_sessions.find( (s) => s.provider_id.startsWith("pp_cryptonly_"))
window.location.href = session.data.paymentPageUrlCheckout label and icon
Section titled “Checkout label and icon”Checkout labels and icons are storefront-owned — Medusa does not expose them from the provider. On the Next.js starter, add an entry to paymentInfoMap in src/lib/constants.tsx:
import Cryptonly from "@modules/common/icons/cryptonly"
pp_cryptonly_cryptonly: { title: "Cryptonly (crypto)", icon: <Cryptonly />,},A reference icon component ships with the plugin under assets/storefront/cryptonly.tsx, and PNG/SVG brand marks are available in assets/ as well.
Status mapping
Section titled “Status mapping”The plugin listens for invoice.statusChanged webhooks and maps Cryptonly statuses to Medusa results as follows:
| Cryptonly status | Medusa result |
|---|---|
paid, overpaid | Captured (successful) |
created, processing | Pending (no completion) |
partially_paid | Failed |
failed, expired, suspended | Failed |
cancelled | Canceled |
Live demo (sandbox)
Section titled “Live demo (sandbox)”Try the full flow - no real funds needed: medusa-demo.cryptonly.net