@open-border/medusa-payment-openborder
v0.6.0
Published
Medusa payment and tax provider bridge for Open Border.
Readme
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.
Package pages
- npmjs public package:
@open-border/medusa-payment-openborder - GitHub Packages package:
@openborder/medusa-payment-openborder - Browser checkout element:
@open-border/js - Node SDK:
@open-border/node
Install
Use npmjs for public installs:
npm install @open-border/medusa-payment-openborder @open-border/jsGitHub Packages consumers can install the GitHub-scoped package instead:
npm install @openborder/medusa-payment-openborderGitHub Packages requires npm registry configuration and a token with package read access:
@openborder:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
always-auth=trueConfiguration
Configure the Medusa server with an Open Border secret key. Keep this key server-side only.
# Local development
OPENBORDER_API_URL=http://localhost:3000
OPENBORDER_API_KEY=sk_test_dev
OPENBORDER_PUBLISHABLE_KEY=pk_test_dev
# Sandbox / staging
# OPENBORDER_API_URL=https://api-demo.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:
OPENBORDER_API_KEYis thesk_...key. It belongs on the Medusa server and is used by the plugin to quote tax/duty and create payment intents.OPENBORDER_PUBLISHABLE_KEYis thepk_...key. It is safe for browser code and is used by@open-border/jsto tokenize the buyer's card into apm_...payment method.- Test and live rails do not mix. Use
sk_test_...withpk_test_..., andsk_live_...withpk_live_.... 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.
// medusa-config.ts
module.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 openborder. With the provider id also set to openborder,
Medusa stores the resolved provider id as pp_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
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
Open Border amounts are integer minor units. Each line item needs an HS tariff code before the tax quote can be created.
const 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:
const {
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_postal_code: shippingAddress.postal_code,
currency: cart.currency_code.toUpperCase(),
shipping_amount: cart.shipping_total,
customer: { email: cart.email },
});quote.amount_breakdown.total is the landed-cost total to display before card collection.
quote.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 @open-border/js. The browser element only tokenizes the card. It
does not charge the buyer and never receives the secret key.
<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({
cart_id: cart.id,
payment_method: paymentMethodId,
tax_quote_id: quote.tax_quote_id,
}),
});
},
onError: (message) => console.error(message),
});
</script>Only expose OPENBORDER_PUBLISHABLE_KEY and, when needed, a non-secret browser API URL. Do not
serialize OPENBORDER_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:
const sessionData = {
payment_method: paymentMethodId,
openborder_tax_quote_id: quote.tax_quote_id,
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:
payment_methodoropenborder_payment_method- thepm_...token from the browser element.openborder_tax_quote_idortax_quote_id- the server-issued quote id.line_items- the same item fingerprint used for the quote.shipping_amount- integer minor units, when shipping was included in the quote.customer.emailbilling_address.line1andbilling_address.countryshipping_address.line1andshipping_address.countrymerchant_reference- usually the Medusa cart, order, or payment collection id.
Provider behavior
The payment provider maps a Medusa-shaped payment-session request into a manual-capture
createPaymentIntent, so Medusa can authorize first and capture or cancel through its later payment
lifecycle. Medusa passes the provider amount in major units (for example, 129.99 for USD), so
the provider converts that boundary value to Open Border minor units before creating or refunding an
intent. The tax provider maps Medusa-shaped line items and destination context into
createTaxQuote, 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.
Capture, cancel, and refund map to Open Border payment actions with caller-supplied idempotency keys. The Medusa wrapper derives distinct Open Border idempotency keys for create/update, capture, cancel, and refund actions so a persisted payment-session key is not reused for different request bodies. Status lookup reads the current Open Border payment-intent status and does not own local money state.
Money-mutating provider calls require an idempotency key from Medusa context or session data. Forward Medusa's request idempotency key when creating, capturing, canceling, or refunding.
If the provider is called before a payment method exists, it returns a pending session with
openborder_requires_payment_method: true. After the browser returns a pm_... 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:
openborder_payment_intent_idopenborder_statusentity- the Open Border entity resolved from charge currency.amount_breakdown- subtotal, shipping, tax, duty, total, and currency.tax_quote_idclient_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:
# Open Border API
pnpm dev
# Medusa app
OPENBORDER_API_URL=http://localhost:3000
OPENBORDER_API_KEY=sk_test_dev
OPENBORDER_PUBLISHABLE_KEY=pk_test_devSandbox or internal staging:
OPENBORDER_API_URL=https://api-demo.openborderpayments.com
OPENBORDER_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.
Private demo store vs public package guide
The repository also contains a private demo store under examples/medusa-demo. That demo is for
internal presentation and local smoke testing. It uses /api/demo/* helper routes and a preview
mode so the flow is reliable for standups.
This README is the public integration guide for package consumers. Do not document demo-only helper routes in Scalar or treat the private demo server as the required public 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
@open-border/js; this plugin does not render checkout UI by itself. - Medusa webhook action handling currently returns
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 and require caller-supplied idempotency keys where money moves.
- 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.
Security checklist
- Keep
sk_...keys server-side. - Expose only
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.
