Платёжный и налоговый провайдер Medusa для Open Border.
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 . 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.
A tax quote is now required on every charge. Open Border rejects a payment intent with no as a , 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 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 minor (), never a patch.
The unified Medusa major-unit boundary is a breaking change for the sibling money fields. The first release containing it must be a new minor ( or later), never a patch.
In , the provider already treated Medusa's payment as major units, but and were documented and forwarded as Open Border minor units. Starting with , pass all three as Medusa major-unit values:
| Field | input | input |
|---|---|---|
| Payment total | ||
| Server | 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 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 the public npm packages. No or registry token is required:
Configure the Medusa server with an Open Border secret key. Keep this key server-side only.
Key rules:
Add the Open Border provider to the Medusa v2 Payment Module.
The provider identifier is . With the provider also set to , Medusa stores the resolved provider id as .
The integration is quote-before-pay:
Use Medusa's major-unit values at the plugin boundary (for example, 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.
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.
The plugin exposes a thin Open Border client seam for testable Medusa integrations:
US and CA destinations require at least one of or . Forward the Medusa shipping address values as shown; other destinations may omit both.
is the landed-cost total to display before card collection. must travel with the payment session so Open Border can revalidate the quote when creating the payment intent.
Use the publishable key with . The browser element only tokenizes the card. It does not charge the buyer and never receives the secret key.
Only expose and, when needed, a non-secret browser API URL. Do not serialize into HTML, JavaScript, logs, analytics, or error telemetry.
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:
Required values:
Optional values:
The payment provider maps a Medusa-shaped payment-session request into a manual-capture , so Medusa can authorize first and capture or cancel through its later payment lifecycle. Medusa passes money in major units (for example, 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 because Medusa's is the full payment-collection total, including shipping and quoted tax or duty. Every payment requires the server-issued ; the provider checks its total, subtotal, shipping, and currency before authorization. The tax provider maps the normalized line items and destination context into , 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 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 . After the browser returns a token, update the session and the provider will create the Open Border payment intent.
After a successful payment-intent create, persist the returned Open Border ids alongside the Medusa order/payment collection:
These fields let support staff reconcile Medusa orders to Open Border transactions without making Medusa the source of truth for money.
Local development:
Sandbox or internal staging:
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.
The demo source is maintained publicly in . 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.
npm install @open-border/medusa-payment-openborder @open-border/js1# Local development2OPENBORDER_API_URL=http://localhost:30003OPENBORDER_API_KEY=sk_test_dev4OPENBORDER_PUBLISHABLE_KEY=pk_test_dev5
6# Sandbox / staging7# OPENBORDER_API_URL=https://api-sandbox.openborderpayments.com8# OPENBORDER_API_KEY=sk_test_...9# OPENBORDER_PUBLISHABLE_KEY=pk_test_...10
11# Production12# OPENBORDER_API_URL=https://api.openborderpayments.com13# OPENBORDER_API_KEY=sk_live_...14# OPENBORDER_PUBLISHABLE_KEY=pk_live_...1// medusa-config.ts2module.exports = {3 modules: [4 {5 resolve: '@medusajs/medusa/payment',6 options: {7 providers: [8 {9 resolve: '@open-border/medusa-payment-openborder/providers/openborder',10 id: 'openborder',11 options: {12 apiKey: process.env.OPENBORDER_API_KEY,13 baseUrl: process.env.OPENBORDER_API_URL,14 },15 },16 ],17 },18 },19 ],20};1const openBorderLineItems = cart.items.map((item) => ({2 sku: item.variant?.sku,3 description: item.title,4 quantity: item.quantity,5 unit_amount: item.unit_price,6 hs_code: item.metadata?.hs_code,7}));1const {2 createOpenBorderApiClient,3 OpenBorderTaxProvider,4} = require('@open-border/medusa-payment-openborder');5
6const openBorder = createOpenBorderApiClient({7 apiKey: process.env.OPENBORDER_API_KEY,8 baseUrl: process.env.OPENBORDER_API_URL,9});10const taxProvider = new OpenBorderTaxProvider(openBorder);11
12const quote = await taxProvider.getTaxLines(openBorderLineItems, {13 destination_country: shippingAddress.country_code.toUpperCase(),14 destination_region: shippingAddress.province,15 destination_postal_code: shippingAddress.postal_code,16 ship_from_country: 'US', // your dispatch origin — the other half of the priced lane17 currency: cart.currency_code.toUpperCase(),18 shipping_amount: cart.shipping_total,19 customer: { email: cart.email },20});1<div id="openborder-checkout"></div>2<script src="https://unpkg.com/@open-border/js"></script>3<script>4 const checkout = OpenBorder(window.OPENBORDER_PUBLISHABLE_KEY, {5 apiBaseUrl: window.OPENBORDER_API_URL,6 });7
8 checkout.mount('#openborder-checkout', {9 currency: quote.amount_breakdown.currency,10 amount: quote.amount_breakdown.total,11 billingDetails: {12 email: cart.email,13 name: cart.shipping_address?.first_name,14 address: cart.shipping_address,15 },16 onSuccess: async ({ paymentMethodId }) => {17 await fetch('/store/checkout/openborder-payment-method', {18 method: 'POST',19 headers: { 'Content-Type': 'application/json' },20 body: JSON.stringify({21 cart_id: cart.id,22 payment_method: paymentMethodId,23 tax_quote_id: quote.tax_quote_id,24 }),25 });26 },27 onError: (message) => console.error(message),28 });29</script>1const sessionData = {2 cart_id: cart.id,3 payment_method: paymentMethodId,4 openborder_tax_quote_id: quote.tax_quote_id,5 amount_breakdown: quote.amount_breakdown,6 shipping_amount: cart.shipping_total,7 merchant_reference: cart.id,8 customer: {9 email: cart.email,10 name: `${cart.shipping_address.first_name} ${cart.shipping_address.last_name}`.trim(),11 },12 billing_address: toOpenBorderAddress(cart.billing_address ?? cart.shipping_address),13 shipping_address: toOpenBorderAddress(cart.shipping_address),14 line_items: openBorderLineItems,15 metadata: {16 medusa_cart_id: cart.id,17 },18};1# Open Border API2pnpm dev3
4# Medusa app5OPENBORDER_API_URL=http://localhost:30006OPENBORDER_API_KEY=sk_test_dev7OPENBORDER_PUBLISHABLE_KEY=pk_test_dev1OPENBORDER_API_URL=https://api-sandbox.openborderpayments.com2OPENBORDER_API_KEY=sk_test_...3OPENBORDER_PUBLISHABLE_KEY=pk_test_...