Von Payments for Medusa
Take card payments in a Medusa v2 store. The buyer types their card into a secure field served by the payment processor inside your storefront's checkout, and the card is charged when they place the order. Refunds and cancellations from the Medusa admin move the money at Von Payments.
This page is for the developer setting up the store. You need access to the Medusa backend's configuration and to the storefront code.
- Refund from Medusa, not from the Von Payments dashboard. A refund made in the dashboard does not reach your store, so the order would still show the money as taken.
- The Redis modules and a worker are required. Payment notifications and the plugin's background checks run on Medusa's worker, and payments are only protected against being completed twice when the server and the worker share Redis. An in-memory setup is not supported.
Before you install
- Medusa 2.15.5 or newer, on Node 20.19 or newer (or 22.12 or newer).
- Medusa's Redis event bus, Redis workflow engine, Redis locking and Redis cache modules, and a Medusa process in
worker(orshared) mode. Server and worker must use the sameREDIS_URL, database number included, and Redis must not evict keys (maxmemory-policy noeviction): an evicting Redis silently drops queued events and locks. - Your Von Payments API keys: a secret key for the backend and a publishable key for the storefront, in the same mode.
- Your storefront served over HTTPS.
Install
You set the plugin's keys and addresses as environment settings, not in the Medusa admin:
| Setting | Where | What goes there |
|---|---|---|
VONPAY_SECRET_KEY | Server .env | Your secret key, vp_sk_test_… or vp_sk_live_…. |
VONPAY_MODE | Server .env | test or live, matching the key. |
VONPAY_STOREFRONT_URL | Server .env | Your storefront's address. |
VONPAY_WEBHOOK_SECRET | Server .env | The signing secret of your payment-updates endpoint, once you have created it (step 6). |
MEDUSA_BACKEND_URL | Server .env | Your backend's public address. |
NEXT_PUBLIC_VONPAY_PUBLISHABLE_KEY | Storefront environment | Your publishable key, vp_pk_…, in the same mode. Never the secret key. |
VONPAY_SHOPPER_IP_HEADER | Storefront environment | Optional, for one-click offers: the header your hosting sets to the shopper's address. |
In the Medusa admin you add the payment provider to your regions (step 5) and run the setup check (step 8).
Do these steps in order. Each one says where it happens.
-
In your Medusa server project, install the plugin and Medusa's Redis caching provider:
npm install vonpay-medusa @medusajs/caching-redis -
Set the environment in the server's
.env(never in a committed file). The server and the worker both need it:VONPAY_SECRET_KEY=vp_sk_test_...VONPAY_WEBHOOK_SECRET= # empty for the first start; set in step 6VONPAY_STOREFRONT_URL=https://shop.example.comVONPAY_MODE=testMEDUSA_BACKEND_URL=https://api.example.comREDIS_URL=redis://... # the same Redis, database number included, for server and worker -
Register the plugin, its payment provider and the Redis modules in
medusa-config.ts:import { defineConfig, Modules } from "@medusajs/framework/utils"const REDIS_URL = process.env.REDIS_URLmodule.exports = defineConfig({projectConfig: {// ...your existing database, CORS and secret settingsredisUrl: REDIS_URL,workerMode: process.env.MEDUSA_WORKER_MODE || "shared", // "server" and "worker" when you run them apart},admin: {backendUrl: process.env.MEDUSA_BACKEND_URL, // lets the setup check confirm the endpoint is this store's},plugins: [{ resolve: "vonpay-medusa", options: {} }],modules: [{key: Modules.PAYMENT,resolve: "@medusajs/medusa/payment",options: {providers: [{resolve: "vonpay-medusa/providers/vonpay",id: "vonpay",options: {secretKey: process.env.VONPAY_SECRET_KEY,webhookSecret: process.env.VONPAY_WEBHOOK_SECRET,storefrontUrl: process.env.VONPAY_STOREFRONT_URL,mode: process.env.VONPAY_MODE, // "test" or "live"},},],},},{key: Modules.CACHING,resolve: "@medusajs/medusa/caching",options: {providers: [{ resolve: "@medusajs/caching-redis", id: "caching-redis", is_default: true, options: { redisUrl: REDIS_URL } },],},},{ key: Modules.EVENT_BUS, resolve: "@medusajs/medusa/event-bus-redis", options: { redisUrl: REDIS_URL } },{ key: Modules.WORKFLOW_ENGINE, resolve: "@medusajs/medusa/workflow-engine-redis", options: { redis: { redisUrl: REDIS_URL } } },{key: Modules.LOCKING,resolve: "@medusajs/medusa/locking",options: {providers: [{ resolve: "@medusajs/medusa/locking-redis", id: "locking-redis", is_default: true, options: { redisUrl: REDIS_URL } },],},},],})Options you add later (Configure) go in the provider's
options. -
Create the plugin's tables and start the server and the worker with that environment:
npx medusa db:migrateA malformed value, or a missing required option other than the signing secret, stops the start with an error naming the option or variable, never its value. The store starts without the signing secret and takes payments, but refuses every payment update until step 6.
-
In the Medusa admin, open Settings → Regions and add Von Payments (
pp_vonpay_vonpay) to the payment providers of each region that should offer it. Checkout never shows it in a region you skip. -
Create the payment-updates endpoint (Payment updates), copy its signing secret into
VONPAY_WEBHOOK_SECRET, and restart the server and the worker. Do this before you take live payments: a payment update the store refused is not sent again. -
In your storefront's folder, add the card step and set
NEXT_PUBLIC_VONPAY_PUBLISHABLE_KEY(Add the card step to your storefront), then deploy the storefront. -
Check it. From the Medusa project folder,
npx vonpay-medusa-checkmust pass, or press Check Vonpay setup under Orders → Held payments (Check the setup). Then test a payment, and switch to live payments when you are ready.
Configure
| Option | Required | What it is |
|---|---|---|
secretKey | Yes | Your secret key, vp_sk_test_… or vp_sk_live_…. |
mode | Yes | test or live. It must match the key; a mismatch stops the store from starting. |
webhookSecret | After setup | The signing secret of your payment-updates endpoint (below). The store starts without it, but refuses every payment update and fails the setup check until it is set. An API key here is refused. |
storefrontUrl | Yes | Your storefront's origin only, https:// (plain http://localhost only in test mode), with no path or country segment. A buyer returning from their bank's 3-D Secure page lands on <storefrontUrl>/checkout/vonpay-return, and the starter's middleware redirects that to the buyer's country path, keeping the query string. |
recovery | No, default true | Every 5 minutes the worker looks for a checkout Von Payments reports paid but Medusa has no order for, and completes it, in case a payment update never arrived. It never refunds. |
holdRecheck | No, default true | Every 5 minutes the worker asks again about a held payment whose outcome was unknown, and takes it off the held list once Von Payments closes it with nothing charged. |
savedCards | No, default off | Lets logged-in customers save a card. Set it to "recurring" if you charge saved cards on a fixed schedule (subscription renewals), or "unscheduled" if you charge them later with no schedule. The value is what Von Payments tells the card networks for every card saved on your store, so it must match how you charge. Logged-in customers then see a "Save my card for future orders" checkbox, never pre-ticked; guests never see it. When you delete a customer in Medusa, their email is erased at Von Payments too. That erase does not remove every copy (for example, payment records keep what settlement needs), so do not describe it to a shopper as a complete deletion; see Erasing a buyer's email. Each customer can keep up to 10 saved cards (Saved cards). |
savedCardsTerms | No, default unset | Your own consent wording, used instead of the checkbox (Consent by your own terms). Needs savedCards. |
wallets | No, default off | Shows Apple Pay and Google Pay above the card form (Apple Pay and Google Pay). |
upsells | No, default off | One-click offers after the order, to signed-in customers and guests; guests: false leaves guests out (One-click add-ons). |
subscriptions | No, default off | Set it to "store" when your own subscription plugin (for example ReorderJS) renews orders: the plugin then charges the customer's saved card when that plugin asks (Subscription renewals). Needs savedCards. |
statementDescriptor | No, default off | What the plugin puts in the per-payment ending of your card-statement line (Statement descriptor). |
An option the plugin cannot accept stops the store from starting with a vonpay_options_* error naming the option, never its value.
Payment updates
In your Von Payments dashboard, create a webhook endpoint at your backend's public address followed by /hooks/payment/vonpay_vonpay, subscribed to charge.succeeded and charge.failed. Use the same public address as MEDUSA_BACKEND_URL, so the setup check can confirm the endpoint is this store's. Copy the signing secret shown when it is created into VONPAY_WEBHOOK_SECRET, then restart the server and the worker. With wallets on, also subscribe it to payment_intent.authorized (or select all events): a wallet payment sends no other update before the order exists, and the setup check warns when it is missing. Keep one endpoint per store: each active endpoint receives every event it is subscribed to, so a second endpoint for the same store delivers each update twice, and the setup check warns when it finds more than one. Test and live keys each need their own endpoint (Switch to live payments).
Medusa answers this route before the plugin checks the signature, so anyone can make your server do that work. At your proxy, rate-limit requests to this one path per client address, leaving room for a burst of payment updates. Keep the route public over HTTPS: the signature check is what stops a forged update.
Add the card step to your storefront
The same vonpay-medusa package carries the storefront card step and the 3-D Secure return page for the Medusa Next.js starter. In your storefront's folder:
npm install vonpay-medusa
npx vonpay-medusa setup-storefront
The command adds two files to a src/lib/vonpay folder in your storefront: actions.ts (the server actions) and index.tsx (the card step and return page). It also adds the 3-D Secure return page at checkout/vonpay-return, and adds transpilePackages: ["vonpay-medusa"] to your Next config and the package's files to Tailwind's content. It is safe to run again, and --dry-run shows what it would do. A file it does not recognise, such as a customised checkout, is left alone and printed as a step for you to make by hand; --force replaces it. Then set the publishable key:
NEXT_PUBLIC_VONPAY_PUBLISHABLE_KEY=vp_pk_test_... # the publishable key only
If you wire the storefront by hand instead: render VonpayCardPayment and VonpayReturn from vonpay-medusa/storefront, build their actions with createVonpayStorefrontServer from vonpay-medusa/storefront/server in a "use server" file of your own, import vonpay-medusa/storefront/card-field.css once, add transpilePackages: ["vonpay-medusa"] (the pieces ship as TypeScript source), and add the package's files to Tailwind's content so their classes are generated.
Card entry appears in the Payment step, directly above Place order. There is no Review step for a Von Payments payment.
The card form loads vora.js from https://js.vonpay.com/v1/vora.js, which updates automatically. Add the sources vora.js needs to your checkout pages' Content Security Policy (CSP requirements).
The card number is entered only inside the vora.js frame and never reaches your storefront, your Medusa server or Von Payments, which is what the plugin is designed around for SAQ-A. Whether your store qualifies also depends on your own checkout page, including the scripts it loads.
Apple Pay and Google Pay
Set wallets: true in the provider options. Then register your storefront's domain, once per store and again if its address changes or you switch between test and live keys. From the Medusa project folder:
npx vonpay-medusa-check --wallet-domains
It registers the host of storefrontUrl for both wallets, saves Apple's domain file in your store's own record, and asks Von Payments to check both. Your storefront serves that file at /.well-known/apple-developer-merchantid-domain-association through the route npx vonpay-medusa setup-storefront adds, so deploy the storefront first, or register again after you do. The setup check shows each wallet's status and compares the file your storefront serves with the saved one. Nothing here moves money.
The buttons appear above the card form only where the shopper's device supports the wallet and the domain is registered for it; anywhere else the checkout is the card form alone.
A wallet payment is held when the shopper approves it and taken once Medusa has the order, so the order shows the payment as captured. If the order cannot be created, or the cart changes, the hold is released and nothing is taken. A shopper who approves and then closes the page still gets an order: the recovery job completes it about 10 minutes after the tap.
If taking the held payment fails (usually a short outage), the plugin keeps retrying for 2 hours. Within about 10 minutes the payment is listed under Held payments as not collected yet and retrying, with nothing for you to do. If the retries end without it, the row becomes Wallet payment not collected, with Open order to capture the payment or cancel the order. Don't ship that order until the payment is captured.
Saved cards
With savedCards set, logged-in customers see Save my card for future orders, never pre-ticked. Each customer can keep up to 10 saved cards; at 10 the checkbox is not offered until they remove one.
npx vonpay-medusa setup-storefront adds a My saved cards page at /account/saved-cards when your storefront has the starter's account area, and prints where to add the menu link. Customers see every card saved for them and can remove one, which revokes it at Von Payments for good. A card the customer did not agree to have shown back is listed as "A saved card", without brand or last digits, and can still be removed. The page uses the plugin's store routes at /store/vonpay/saved-cards, which answer only a signed-in customer, and only about their own cards.
At Von Payments each customer appears as a buyer with the reference medusa_<mode>_<customer id> (for example medusa_live_cus_01…), which is what to search for in the dashboard.
Consent by your own terms
Instead of the Save my card checkbox, you can collect the agreement through your own wording (why that counts as consent, and what it must say). Set savedCardsTerms to the exact wording, 20 to 1,000 characters of plain text, for example: "Your card is saved and charged automatically for this subscription's renewals until you cancel." It needs savedCards.
With it set, the checkout shows no checkbox. On a subscription cart the page shows By placing your order you agree to: followed by your wording, above the Apple Pay and Google Pay buttons when they show, otherwise just above the pay button. Completing that checkout saves the card for the renewals, for a card or a wallet payment alike. A one-time purchase saves nothing. On a vora.js older than 1.43.0, a subscription cart also shows the required checkbox.
The plugin records your configured wording with each payment it applied to, when the payment is confirmed: save_consent_record on the payment's data, with basis: "store_terms", the wording, its SHA-256, whether the cart was a subscription, and recorded_at. Each recorded payment is also written to your server's log as save_consent_recorded, with the payment's ids and the wording's SHA-256 but never the wording itself. It records what the store was set to show, so change the wording deliberately. Cards saved this way appear under My saved cards as "A saved card".
The wording is yours to get right: state the price (or how it is set), how often it renews, that it continues until cancelled, and how to cancel; send your own confirmation email with those terms after the order; and have the wording reviewed by your legal counsel. If in doubt, keep the checkbox.
Subscription renewals
For a store whose own subscription plugin, such as ReorderJS, creates renewal orders. Set savedCards: "recurring" and subscriptions: "store".
On a cart with a subscription line (ReorderJS marks it with metadata.is_subscription):
- The checkbox reads Save my card for future orders, including this subscription's renewals, and the shopper must tick it before paying.
- A guest is asked to sign in, because the card can only be saved to an account.
- A customer can keep up to 50 saved cards for subscriptions.
- Apple Pay and Google Pay appear here too, and save the wallet's card with the same tick (vora.js 1.43.0 or later, which the live channel serves).
The plugin charges a renewal only for a payment session opened with off_session: true (as ReorderJS 1.1.0 does) on an existing order, for that order's own customer, with a saved card in the store's current mode. Anything else is refused and nothing is charged. Each order is charged at most once, and an attempt whose result is unclear goes to Held payments instead of being retried. Before each charge the plugin also checks that Von Payments does not bill your subscriptions itself; if it does, or the answer cannot be read, nothing is charged and renewal_subscription_billing is logged.
A held or refused renewal comes back to ReorderJS as not authorized, so ReorderJS opens a dunning case even when the charge may have gone through. Check the payment in the Von Payments dashboard first. ReorderJS 1.1.0's automatic retry of a failed renewal does not reach the payment provider, so collect a failed renewal another way and mark the case recovered only once the money has arrived.
One-click add-ons
Offer a product right after checkout, paid with one click and added to the order the shopper just placed. Set upsells in the provider options to the product variants you want to offer, 1 to 20 Medusa variant ids:
upsells: { variantIds: ["variant_01...", "variant_01..."] },
Medusa prices each offer, tax included; the browser never sets a price. npx vonpay-medusa setup-storefront adds the offer page at /<country>/order/<id>/offer and its bank-check return page at /<country>/order/<id>/offer/return. Both load vora.js, so give them the same Content Security Policy as your checkout.
What the shopper sees:
- On the payment step: "Your card may be charged for any offer you accept on the next page."
- After paying, the offer page: Add this to your order?, a button to add it at the shown price, and No thanks, view my order. One offer at a time, up to three per order, for about an hour after the order.
- An accepted offer joins the original order as an order edit with its own payment, refundable on its own, and ships with the rest of the order (wait before you ship).
How it is paid:
- Card: each checkout asks Von Payments to keep the card for this purchase only, so the offer is charged without the shopper typing it again. That card is never charged when the shopper is away, is not shown under My saved cards, does not count toward the 10-card limit, and is removed by the plugin once that order's offer has ended.
- Declined: nothing is added and the order is unchanged; the shopper can try again.
- 3-D Secure: the shopper confirms with their bank and comes back to the offer's return page, which shows the outcome.
- Apple Pay and Google Pay: the shopper sees the wallet button straight away. The tap only holds the amount; the item is added and the hold taken, or the hold is released when the item cannot be added (sold out, price moved). Nothing is charged and then refunded.
Your server makes the offer charge, so the processor's fraud checks see your server's address unless the storefront passes on the shopper's. Set VONPAY_SHOPPER_IP_HEADER in the storefront's environment to the header your hosting overwrites on every request with the visitor's own address: x-real-ip on Vercel, cf-connecting-ip behind Cloudflare. Never use x-forwarded-for: a visitor can type its first entry. Left unset, offers are charged without an address. If you wired the storefront by hand, pass the header name as shopperIpHeader to createVonpayStorefrontServer.
Offers go to guests as well as signed-in customers. A guest is known only by the email they type at checkout, so their card is never added to a customer's account, even when they type that customer's email. Set guests: false in upsells to show offers to signed-in customers only; a guest then pays as usual and sees no offer page. Anything the plugin could not finish shows under Held payments with what to do.
Every offer stops about 90 minutes after the order, or as soon as the order is fulfilled, whichever comes first. After that nothing is added to the order. A shopper's bank check still open at that point is closed on the store's side; if the bank approves it afterwards, the plugin refunds that charge in full.
Wait before you ship
While an offer can still add an item, the order page in the admin shows when you can fulfil, and Medusa's Fulfil is refused until then. It is also refused while an offer payment is being confirmed, or when a shopper paid for an offer whose item could not be added: check that one under Held payments. Ship now anyway on the order page lets you fulfil straight away and ends the order's offers. If the plugin cannot read the order's offer state, Fulfil is not blocked.
Email to the shopper
When an offer's item is added, the plugin emails the shopper once, with the subject {Store}: added to your order #{n}. It goes through Medusa's own notifications on the email channel, so configure an email provider in medusa-config; without one nothing is sent and the setup check warns. Set email: false in upsells to send your own instead, or email: { template: "<id>" } to use your provider's template.
Statement descriptor
Your card-statement line is a brand line plus a short per-payment ending. You set both, and which payment field the ending is read from, in your Von Payments dashboard (Statement descriptors). statementDescriptor tells the plugin what to put in that field:
statementDescriptor: { key: "order_id", from: ["product_title", "store_name"] },
key is the field your dashboard rule reads: order_id, subscription_id, customer_id, or your own field name. from lists one to three sources in order, and the first one the payment has is sent; with none, your rule's fallback applies.
from | What is sent |
|---|---|
"product_title" | The product's title; with several lines, the first title plus "+N" (Blue Mug +2). On an offer, the offered product. |
"store_name" | Your Medusa store's name. |
"order_number" | The order number as the admin shows it (#1001). Offers and renewals only. |
"customer_reference" | The plugin's own customer reference (medusa_live_cus_…), for signed-in shoppers and renewals. |
"subscription_reference" | The subscription id your subscription plugin records on a renewal order. |
{ text: "ACME COFFEE" } | Fixed text, 5 to 22 plain characters. Only as the last source. |
One-click offer card charges and renewals carry the ending. Checkout payments, and offers paid with Apple Pay or Google Pay, show your plain descriptor. A key that suggests personal data, such as email or phone, stops the store from starting with an error naming the word it found. The setup check compares this option with your dashboard setting.
Keep the plugin updated
Updating vonpay-medusa updates the server plugin and the storefront pieces together, and releases follow semantic versioning.
When an update is available
The admin's Orders page shows vonpay-medusa X is available (or Security fix: …) with the exact command and a What changed link. Your Medusa server reads this from a file Von Payments signs, at https://www.vonpay.com/medusa/update.json, about twice a day; your store sends nothing about itself, and an unsigned or unreachable file is never shown. With no outbound internet, turn the check off with VONPAY_UPDATE_CHECK=off on the server.
Apply an update
-
In your Medusa server project and in your storefront's folder, install the exact version the notice names:
npm install vonpay-medusa@XName the version:
npm updatestays inside your current version range and can do nothing. Then runnpx medusa db:migratein the Medusa server project. -
Rebuild and redeploy both the Medusa server (and its worker) and the storefront.
-
From the Medusa project folder, run
npx vonpay-medusa-check, then place a test payment.
Automatic updates
npx vonpay-medusa setup-storefront offers to turn on automatic updates (--auto-updates or --no-auto-updates answers without the question). It adds .github/dependabot.yml, a daily check for vonpay-medusa in every folder that installs it (for a workspace with one lock file at the top, that top folder), and a workflow that merges patch releases once your required checks pass. Turn on Settings → General → Allow auto-merge in your repository for that. Minor and major releases wait for you. If you already have a dependabot.yml, it is left alone and the command tells you what to add.
To set it up yourself, let an update bot open the pull requests and merge patch releases on its own:
{
"extends": ["config:recommended"],
"packageRules": [
{
"matchPackageNames": ["vonpay-medusa"],
"matchUpdateTypes": ["patch"],
"automerge": true,
"schedule": ["at any time"]
}
]
}
With Dependabot, add an npm entry for each folder with a lock file that installs the package (the Medusa server and the storefront, or their workspace's top folder when they share one lock file), on a daily schedule, allowing vonpay-medusa, and turn on auto-merge for its pull requests. After an update, redeploy the Medusa server, its worker and the storefront.
Check the setup
From your Medusa project folder (in an image built with medusa build, from .medusa/server):
npx vonpay-medusa-check
Or press Check Vonpay setup on the Held payments page under Orders in the admin. The check confirms the provider is enabled and Von Payments accepts its key, that the signing secret is set, that at least one region offers Von Payments, that a payment-updates endpoint is registered and active, that the Redis modules and the worker are configured, with upsells on that an email provider is configured, with statementDescriptor set that it matches your dashboard setting, and, with a test key, that a test update is actually processed (run it again after 10 seconds to see that step). With a live key it sends nothing.
The endpoint line names the address Von Payments sends to and compares it with MEDUSA_BACKEND_URL (the button also accepts the address you reached the admin at). If it cannot confirm the endpoint is this store's, it warns and sends no test update, because the update could reach another store. --strict also fails when a check could not run.
Test a payment
With a test key, place an order at a test-mode total. A card that needs 3-D Secure sends the buyer to their bank's page and back to /checkout/vonpay-return, which shows the outcome the server reports.
Switch to live payments
Your test keys belong to your business's sandbox, a separate account from your live business with its own payment-updates endpoints and signing secrets, so going live takes a new endpoint as well as new keys:
- In your Von Payments dashboard, on your live business (not the sandbox), create the payment-updates endpoint at the same address with the same events (Payment updates), and copy its signing secret.
- On the server, change these together, since a key and mode that disagree stop the store from starting:
VONPAY_SECRET_KEYto your live secret key,VONPAY_MODE=live, andVONPAY_WEBHOOK_SECRETto the live endpoint's signing secret. Then restart the server and the worker. - In the storefront, set
NEXT_PUBLIC_VONPAY_PUBLISHABLE_KEYto your live publishable key, then rebuild and redeploy it. - With
walletson, register your storefront's domain again withnpx vonpay-medusa-check --wallet-domains(Apple Pay and Google Pay). - Run the setup check. The endpoint line must show the live endpoint at this store's address. With a live key the check sends no test update, so it does not prove updates are processed.
Refunds and cancellations
Refund from the order in the Medusa admin, in full or in part; cancelling a paid order refunds what was captured. A refund made in the Von Payments dashboard is not reflected in Medusa.
Held payments
When a payment cannot be matched to an order safely (the cart changed during payment, the amount does not match, or the store could not create the order), the plugin does not complete the cart and does not refund on its own. It lists the payment on Held payments under Orders, where you can refund it or mark it resolved. Three kinds have their own actions:
- Wallet payment held, no order: the shopper approved Apple Pay or Google Pay but no order was created, so the money is only held. Release drops the hold; Complete the order creates the order and takes the payment.
- Wallet payment held, release failed: the cart changed after the approval and the hold could not be dropped automatically. Release it.
- Renewal charged, order unpaid: the customer's saved card was charged for a renewal but Medusa does not show the payment. The customer has paid: mark the order paid in Medusa, and do not charge again.
Running a second payment provider
You can also offer another payment provider in the same regions, so buyers have a second way to pay. If Von Payments cannot be reached, a Von Payments checkout waits in a "confirming" state rather than failing.
Troubleshooting
| What you see | What to do |
|---|---|
The store does not start, with a vonpay_options_* error | Fix the named option (Configure). vonpay_options_mode_key_mismatch: mode and the key are for different modes. |
| The setup check reports the event bus, workflow engine or locking | Add the Redis module it names to medusa-config.ts and restart both processes. |
| The setup check reports no payment-updates endpoint | Register /hooks/payment/vonpay_vonpay on your backend's public address (Payment updates). |
The setup check fails webhook_secret, or payment updates are refused | Copy the endpoint's signing secret into VONPAY_WEBHOOK_SECRET (Payment updates), then restart the server and the worker. |
The setup check fails regions, or checkout does not offer Von Payments | Add pp_vonpay_vonpay to the payment providers of each region you sell in. |
| The endpoint line warns the address is not this store's | Set MEDUSA_BACKEND_URL to your backend's public address, and register the endpoint at that same address. |
| Orders stay pending after a successful payment | Check that a worker is running and shares the server's Redis, then run the setup check with a test key. The recovery job completes the order within minutes once the worker runs. |
The setup check warns saved_cards_terms | Your store saves cards and sells subscriptions (the subscriptions option is set, or medusa-config lists a subscription plugin such as ReorderJS) with only the Save my card checkbox, which does not show the price, how often it renews, any trial or how to cancel. Set savedCardsTerms to those terms (Consent by your own terms), then restart the server and the worker. |
| A payment appears on Held payments | Open it: it says why. Refund it there, or mark it resolved once you have settled it with the buyer. |