Open Border
Cross-border MoR payments and tax
Open Border Medusa payment plugin
Medusa payment and tax provider bridge for Open Border. Medusa owns the storefront, cart, order, and fulfillment flow; Open Border owns Merchant-of-Record tax, duty, payment-intent, entity-routing, and ledger state.
This package is intentionally thin. It stores no authoritative money state and calls the Open Border API through the Node SDK.
Setup instructions are canonical at Copy to clipboard/guides/medusa. That published page is the step-by-step setup guide; this README stays the detailed integration reference for package consumers. When the two disagree about a setup step, the published guide wins — update it in the same change.Package pages
- npmjs public package: Copy to clipboard
@open-border/medusa-payment-openborder - Browser checkout element: Copy to clipboard
@open-border/js - Node SDK: Copy to clipboard
@open-border/node - Public demo source: Copy to clipboard
OpenBorder/openborder-medusa-demo
Upgrading from 0.7.x
A tax quote is now required on every charge. Open Border rejects a payment intent with no Copy to clipboardtax_quote_id as a Copy to clipboardvalidation_error, so a cart that reaches payment-session creation without a quote id fails closed in the plugin rather than charging an untaxed total. Run the Open Border tax provider on the cart before creating the payment session, and keep the server-issued Copy to clipboardamount_breakdown alongside the quote id.
If your storefront relied on creating a session for an unquoted cart, that path no longer produces a charge — quote first. This narrows the published input contract, so the first release containing it is a breaking Copy to clipboard0.x minor (Copy to clipboard0.8.0), never a Copy to clipboard0.7.x patch.
Upgrading from 0.6.x
The unified Medusa major-unit boundary is a breaking change for the sibling money fields. The first release containing it must be a new Copy to clipboard0.x minor (Copy to clipboard0.7.0 or later), never a Copy to clipboard0.6.x patch.
In Copy to clipboard0.6.x, the provider already treated Medusa's payment Copy to clipboardamount as major units, but Copy to clipboardshipping_amount and Copy to clipboardline_items[].unit_amount were documented and forwarded as Open Border minor units. Starting with Copy to clipboard0.7.0, pass all three as Medusa major-unit values:
Field Copy to clipboard0.6.x input Copy to clipboard0.7.0 input Payment total Copy to clipboard160.30 Copy to clipboard160.30 Copy to clipboardshipping_amount Copy to clipboard990 Copy to clipboard9.90 Copy to clipboardline_items[].unit_amount Copy to clipboard12900 Copy to clipboard129.00 Server Copy to clipboardamount_breakdown.* minor units minor units
Update the payment-session and tax-provider inputs atomically before upgrading. Integer major and minor values are ambiguous, so the plugin cannot safely auto-detect a mixed old/new payload. Quoted payment sessions must also retain the server-issued Copy to clipboardamount_breakdown alongside the quote id so the provider can reconcile Medusa's payment total before authorization. Keep that breakdown exactly as returned; its fields remain integer minor units.
Install
Install the public npm packages. No Copy to clipboard.npmrc or registry token is required:
1npm install @open-border/medusa-payment-openborder @open-border/js
Configuration
Configure the Medusa server with an Open Border secret key. Keep this key server-side only.
1234567891011121314# Local developmentOPENBORDER_API_URL=http://localhost:3000OPENBORDER_API_KEY=sk_test_devOPENBORDER_PUBLISHABLE_KEY=pk_test_dev# Sandbox / staging# OPENBORDER_API_URL=https://api-sandbox.openborderpayments.com# OPENBORDER_API_KEY=sk_test_...# OPENBORDER_PUBLISHABLE_KEY=pk_test_...# Production# OPENBORDER_API_URL=https://api.openborderpayments.com# OPENBORDER_API_KEY=sk_live_...# OPENBORDER_PUBLISHABLE_KEY=pk_live_...
Key rules:
- Copy to clipboard
OPENBORDER_API_KEYis the Copy to clipboardsk_...key. It belongs on the Medusa server and is used by the plugin to quote tax/duty and create payment intents. - Copy to clipboard
OPENBORDER_PUBLISHABLE_KEYis the Copy to clipboardpk_...key. It is safe for browser code and is used by Copy to clipboard@open-border/jsto tokenize the buyer's card into a Copy to clipboardpm_...payment method. - Test and live rails do not mix. Use Copy to clipboard
sk_test_...with Copy to clipboardpk_test_..., and Copy to clipboardsk_live_...with Copy to clipboardpk_live_.... - Copy to clipboard
OPENBORDER_API_URLis optional outside local development because the SDK can choose the API host from the key rail. Pass it explicitly for local or internal staging environments.
Register the payment provider
Add the Open Border provider to the Medusa v2 Payment Module.
1234567891011121314151617181920// medusa-config.tsmodule.exports = {modules: [{resolve: '@medusajs/medusa/payment',options: {providers: [{resolve: '@open-border/medusa-payment-openborder/providers/openborder',id: 'openborder',options: {apiKey: process.env.OPENBORDER_API_KEY,baseUrl: process.env.OPENBORDER_API_URL,},},],},},],};
The provider identifier is Copy to clipboardopenborder. With the provider Copy to clipboardid also set to Copy to clipboardopenborder, Medusa stores the resolved provider id as Copy to clipboardpp_openborder_openborder.
Checkout flow
The integration is quote-before-pay:
- Build Open Border line items from the Medusa cart.
- Quote tax/duty for the buyer's ship-to destination.
- Mount the browser checkout element with the quoted landed-cost total.
- Send the resulting Copy to clipboard
pm_...payment method back to the Medusa server. - Create or update the Medusa payment session with the payment method and quote id.
- The Open Border provider creates the payment intent server-side.
- Store the Open Border payment intent id and show the buyer the receipt/order ids.
1. Build quote line items
Use Medusa's major-unit values at the plugin boundary (for example, Copy to clipboard25.00 for USD). The plugin converts every amount to Open Border's integer minor-unit contract after exactness checks. Each line item also needs an HS tariff code before the tax quote can be created.
1234567const openBorderLineItems = cart.items.map((item) => ({sku: item.variant?.sku,description: item.title,quantity: item.quantity,unit_amount: item.unit_price,hs_code: item.metadata?.hs_code,}));
If your catalog does not already store HS codes, classify products with Open Border before quoting and persist the resulting code back to your product/variant metadata.
2. Quote tax and duty
The plugin exposes a thin Open Border client seam for testable Medusa integrations:
1234567891011121314151617181920const {createOpenBorderApiClient,OpenBorderTaxProvider,} = require('@open-border/medusa-payment-openborder');const openBorder = createOpenBorderApiClient({apiKey: process.env.OPENBORDER_API_KEY,baseUrl: process.env.OPENBORDER_API_URL,});const taxProvider = new OpenBorderTaxProvider(openBorder);const quote = await taxProvider.getTaxLines(openBorderLineItems, {destination_country: shippingAddress.country_code.toUpperCase(),destination_region: shippingAddress.province,destination_postal_code: shippingAddress.postal_code,ship_from_country: 'US', // your dispatch origin — the other half of the priced lanecurrency: cart.currency_code.toUpperCase(),shipping_amount: cart.shipping_total,customer: { email: cart.email },});
US and CA destinations require at least one of Copy to clipboarddestination_region or Copy to clipboarddestination_postal_code. Forward the Medusa shipping address values as shown; other destinations may omit both.
Copy to clipboardquote.amount_breakdown.total is the landed-cost total to display before card collection. Copy to clipboardquote.tax_quote_id must travel with the payment session so Open Border can revalidate the quote when creating the payment intent.
3. Collect the payment method in the browser
Use the publishable key with Copy to clipboard@open-border/js. The browser element only tokenizes the card. It does not charge the buyer and never receives the secret key.
1234567891011121314151617181920<div id="openborder-checkout"></div><script src="https://unpkg.com/@open-border/js"></script><script>const checkout = OpenBorder(window.OPENBORDER_PUBLISHABLE_KEY, {apiBaseUrl: window.OPENBORDER_API_URL,});checkout.mount('#openborder-checkout', {currency: quote.amount_breakdown.currency,amount: quote.amount_breakdown.total,billingDetails: {email: cart.email,name: cart.shipping_address?.first_name,address: cart.shipping_address,},onSuccess: async ({ paymentMethodId }) => {await fetch('/store/checkout/openborder-payment-method', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({
Only expose Copy to clipboardOPENBORDER_PUBLISHABLE_KEY and, when needed, a non-secret browser API URL. Do not serialize Copy to clipboardOPENBORDER_API_KEY into HTML, JavaScript, logs, analytics, or error telemetry.
4. Create or update the payment session
When the buyer submits the card element, put the Open Border payment method and quote data into the Medusa payment-session data. The provider reads these fields:
123456789101112131415161718const sessionData = {cart_id: cart.id,payment_method: paymentMethodId,openborder_tax_quote_id: quote.tax_quote_id,amount_breakdown: quote.amount_breakdown,shipping_amount: cart.shipping_total,merchant_reference: cart.id,customer: {email: cart.email,name: `${cart.shipping_address.first_name} ${cart.shipping_address.last_name}`.trim(),},billing_address: toOpenBorderAddress(cart.billing_address ?? cart.shipping_address),shipping_address: toOpenBorderAddress(cart.shipping_address),line_items: openBorderLineItems,metadata: {medusa_cart_id: cart.id,},};
Required values:
- Copy to clipboard
cart_id, Copy to clipboardsession_id, or Copy to clipboardmedusa_session_id- a persisted Medusa identity that remains stable across retries of the same payment session. - Copy to clipboard
payment_methodor Copy to clipboardopenborder_payment_method- the Copy to clipboardpm_...token from the browser element. - Copy to clipboard
openborder_tax_quote_idor Copy to clipboardtax_quote_id- the server-issued quote id. Required: Open Border prices every charge from a revalidated quote, so a cart with no quote is refused before any authorization. - Copy to clipboard
amount_breakdown- the server-issued quote breakdown, retained in minor units so the provider can reconcile Medusa's payment-collection total before authorization. - Copy to clipboard
line_items- the same item fingerprint used for the quote. - Copy to clipboard
shipping_amount- a Medusa major-unit value, when shipping was included in the quote. - Copy to clipboard
customer.email - Copy to clipboard
billing_address.line1and Copy to clipboardbilling_address.country - Copy to clipboard
shipping_address.line1and Copy to clipboardshipping_address.country
Optional values:
- Copy to clipboard
merchant_reference- usually the Medusa cart, order, or payment collection id. When omitted, the provider uses the required stable session identity.
Provider behavior
The payment provider maps a Medusa-shaped payment-session request into a manual-capture Copy to clipboardcreatePaymentIntent, so Medusa can authorize first and capture or cancel through its later payment lifecycle. Medusa passes money in major units (for example, Copy to clipboard160.30 for USD), so the payment and tax providers exactness-check and convert the payment-collection total, shipping amount, and every line-item unit amount to Open Border minor units. The payment provider derives Open Border's merchandise subtotal from Copy to clipboardsum(quantity * unit_amount) because Medusa's Copy to clipboardamount is the full payment-collection total, including shipping and quoted tax or duty. Every payment requires the server-issued Copy to clipboardamount_breakdown; the provider checks its total, subtotal, shipping, and currency before authorization. The tax provider maps the normalized line items and destination context into Copy to clipboardcreateTaxQuote, returning the Open Border quote id and aggregate quote response. The plugin does not allocate tax or duty per line locally; Open Border remains the authoritative pricing source.
The Medusa wrapper derives operation-scoped Open Border idempotency keys. Initiate and update use the persisted payment-session identity, prior intent id, and canonical request body; capture and cancel use the persisted Open Border payment-intent id. A changed Copy to clipboardcontext.idempotency_key on a retry does not change those keys. Refunds keep their existing behavior: a stable Medusa refund reference is preferred, with the request context key used only when no refund reference exists. Status lookup reads the current Open Border payment-intent status and does not own local money state. An update cancels a live authorization before creating its replacement, and a canceled intent can be replaced. An update against a captured intent fails closed before cancellation or re-authorization so a completed payment cannot produce a second buyer hold.
If the provider is called before a payment method exists, it returns a pending session with Copy to clipboardopenborder_requires_payment_method: true. After the browser returns a Copy to clipboardpm_... token, update the session and the provider will create the Open Border payment intent.
Receipt fields
After a successful payment-intent create, persist the returned Open Border ids alongside the Medusa order/payment collection:
- Copy to clipboard
openborder_payment_intent_id - Copy to clipboard
openborder_status - Copy to clipboard
entity- the Open Border entity resolved from charge currency. - Copy to clipboard
amount_breakdown- subtotal, shipping, tax, duty, total, and currency. - Copy to clipboard
tax_quote_id - Copy to clipboard
client_secret, when returned by the payment processor flow.
These fields let support staff reconcile Medusa orders to Open Border transactions without making Medusa the source of truth for money.
Local and staging test mode
Local development:
1234567# Open Border APIpnpm dev# Medusa appOPENBORDER_API_URL=http://localhost:3000OPENBORDER_API_KEY=sk_test_devOPENBORDER_PUBLISHABLE_KEY=pk_test_dev
Sandbox or internal staging:
123OPENBORDER_API_URL=https://api-sandbox.openborderpayments.comOPENBORDER_API_KEY=sk_test_...OPENBORDER_PUBLISHABLE_KEY=pk_test_...
Use test card numbers from the payment processor account attached to the resolved Open Border entity. The publishable key must belong to the same test rail as the secret key.
Demo preview vs public package guide
The demo source is maintained publicly in Copy to clipboardOpenBorder/openborder-medusa-demo. Its deterministic, keyless static build is hosted as a public preview and does not submit a real payment.
This README is the public integration guide for package consumers. Do not document the private demo-only helper routes in Scalar or treat the hosted preview as the required package integration path. External developers should install the package, register the provider, quote tax/duty server-side, collect a browser payment method with the publishable key, and let Open Border create the payment intent with the secret key on the Medusa server.
Current limitations
- The browser payment element comes from Copy to clipboard
@open-border/js; this plugin does not render checkout UI by itself. - Medusa webhook action handling currently returns Copy to clipboard
not_supported. Wire provider webhooks and async reconciliation in a separate integration slice. - Capture, cancel, refund, and payment-status lookup delegate to Open Border APIs. Capture and cancel keys are intent-derived; refunds need a stable refund reference or a request context key.
- The plugin does not allocate tax or duty per line locally. Open Border's quote and transaction snapshot are authoritative.
- The plugin does not persist Open Border ids into Medusa order metadata for you; do that in your checkout/order completion workflow.
- Copy to clipboard
OpenBorderApiClient.reportFulfillment(...), Copy to clipboardcorrectFulfillment(...), and Copy to clipboardgetFulfillment(...)are explicit server-side fulfillment seams. Responses keep OpenBorder's fulfillment state separate from nullable advisory Ship24 state in Copy to clipboardtracking_provider. The Copy to clipboardtracking_numberis required; Copy to clipboardcarrieris optional and may be omitted for Ship24 auto-detection. Copy to clipboardreportFulfillmentalso requires Copy to clipboard{ idempotencyKey }because accepted tracking can release held funds. The package does not install an automatic Medusa lifecycle subscriber or guess fulfillment events.
Security checklist
- Keep Copy to clipboard
sk_...keys server-side. - Expose only Copy to clipboard
pk_...keys to browser code. - Use test keys and sandbox URLs until production activation is approved.
- Send payment-method tokens from the browser to your backend over HTTPS.
- Never log full API keys, payment method ids, card data, or customer addresses in public logs.
- Treat Open Border's payment intent, transaction snapshot, and amount breakdown as the authoritative money record.

