Appearance
Checkout rates
Navexa can quote live shipping rates at your store's checkout from the carriers you set up in Carriers. Settings live under Settings → Live rates (owners and admins): a shipping buffer and pick-and-pack fee added to each rate, free shipping on the cheapest rate above a cart subtotal, how rates appear to customers, the order cutoff and handling days that delivery dates count from, and the default package used to quote.
Shopify
Shopify asks Navexa for rates through a carrier service.
- In Navexa, open Settings → Live rates and choose Enable live rates.
- Your Shopify plan needs third-party carrier-calculated shipping (Advanced or Plus, or a paid add-on). See Shopify's help.
- In Shopify admin, open Settings → Shipping and delivery. Under your shipping profile, pick a zone and choose Add rate.
- Choose Use carrier or app to calculate rates, select Navexa Shipping as the carrier service and tick the services to offer.
- Leave Handling fee and Handling fee percentage blank in Shopify. Navexa already adds your shipping buffer and pick-and-pack fee, so a Shopify handling fee charges customers twice.
Medusa
The Navexa Medusa plugin adds a navexa fulfillment provider that quotes checkout rates, and keeps orders, products, inventory and locations in sync with Navexa. It needs Medusa 2.21.1 or later, Node.js 20 or later, and a Medusa backend reachable over HTTPS (Navexa calls it back).
Install
The plugin is @conradkurth/navexa-medusa-plugin on GitHub Packages. Use 0.6.24 or later: earlier versions don't sign rate requests (see Request signing). GitHub Packages wants a token even for public packages:
Create a GitHub personal access token (classic) with the
read:packagesscope.Add an
.npmrcto your Medusa project (commit it, never the token):ini@conradkurth:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}Install, then run the plugin's migration and restart Medusa:
bashexport GITHUB_PACKAGES_TOKEN=<your token> npm install @conradkurth/navexa-medusa-plugin npx medusa db:migrate
Your CI and Docker builds need the same .npmrc and token. A 403 or 404 on install means your GitHub account can't read the package yet: email support.
Connect
In Medusa Admin, create a secret API key (Settings → Secret API Keys).
In Navexa, go to Integrations → Medusa → Connect and enter your Medusa backend's public
https://URL, that key, and a webhook secret (any long random string, for example fromopenssl rand -hex 32; leave it blank to have one generated).Navexa shows the webhook secret, the plugin API token and the integration id once. Copy them, then register the plugin in
medusa-config.tswith the same options on the module and the fulfillment provider:tsimport { defineConfig } from "@medusajs/framework/utils" const navexa = { api_url: "https://api.navexa.co", navexa_base_url: "https://api.navexa.co", integration_id: process.env.NAVEXA_INTEGRATION_ID, api_token: process.env.NAVEXA_API_TOKEN, webhook_secret: process.env.NAVEXA_WEBHOOK_SECRET, } export default defineConfig({ // ...your existing config plugins: [{ resolve: "@conradkurth/navexa-medusa-plugin", options: navexa }], modules: [ { resolve: "@conradkurth/navexa-medusa-plugin/modules/navexa", options: navexa }, { resolve: "@medusajs/medusa/fulfillment", options: { providers: [ // keep the providers you already use { resolve: "@conradkurth/navexa-medusa-plugin/providers/navexa-fulfillment", id: "navexa", options: navexa }, ], }, }, ], })Redeploy. Navexa starts a full sync (locations, products, orders) as soon as you connect.
In Medusa Admin (Settings → Locations & Shipping), create shipping options on the
navexaprovider. It offersNext Business Day Delivery,Express,StandardandFree Shipping; each shows at checkout only when Navexa returns a rate with that name for the cart.
Settings → Navexa in Medusa Admin shows the connection status, and can hold the credentials instead of env vars. It only drives the sync module: checkout rates always use the options in medusa-config.ts.
If Navexa's carrier quote fails, Navexa answers with fallback rates (the route's last good quote, else 90-day average prices). If the plugin can't reach Navexa at all, it uses the cart's last good quote from the past 24 hours, else its fallback_rates_cents option (prices in cents per service name, for example { "Standard": 899 }).
No Navexa rates at checkout?
- Check
api_url,integration_idandwebhook_secreton the fulfillment provider inmedusa-config.ts; the admin page doesn't affect it. - Check the shipping options use the
navexaprovider, and that Navexa has carriers and a ship-from location. - If your account requires signed requests, the plugin must be 0.6.24 or later with the current webhook secret.
Request signing
Every rate request to Navexa can be signed so nobody else can ask for rates in your name. The Medusa plugin does this for you.
- Header
X-Navexa-Signature: the HMAC-SHA256 of the raw request body, keyed by the integration's webhook secret, as lowercase hex. - Sign the exact bytes you send. Re-encoding the JSON after signing breaks the signature.
- Until signed requests are required for your account, an unsigned or wrongly signed request is still answered (and logged). An owner or admin turns the requirement on under Settings → Shipping controls. After that such a request gets HTTP 401 and no rates.
- Only Medusa integrations have a webhook secret. Requests for other integrations can't be signed, so they stop getting rates once signing is required.
- Regenerating the webhook secret in Navexa stops the old one at once: update the plugin straight away.
js
import crypto from "node:crypto"
const body = JSON.stringify(rateRequest)
const signature = crypto
.createHmac("sha256", process.env.NAVEXA_WEBHOOK_SECRET)
.update(body)
.digest("hex")
await fetch(`https://api.navexa.co/v1/rates/${integrationId}`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Navexa-Signature": signature },
body, // the exact string you signed
})The rates endpoint
POST https://api.navexa.co/v1/rates/{integration_id} is what the Medusa plugin calls. Any platform can call it the same way.
Request
json
{
"origin": {
"address1": "123 Warehouse Blvd",
"city": "Austin",
"province": "TX",
"postal_code": "78701",
"country": "US"
},
"destination": {
"address1": "456 Oak Ave",
"city": "New York",
"province": "NY",
"postal_code": "10001",
"country": "US"
},
"items": [
{
"name": "Widget",
"sku": "WID-001",
"quantity": 2,
"grams": 500,
"price": 1999,
"length_inches": 6.0,
"width_inches": 4.0,
"height_inches": 2.0
}
],
"currency": "USD",
"subtotal_cents": 3998
}price: item price in cents.grams: item weight, 0 if unknown.subtotal_cents: the cart subtotal in cents, used for the free shipping threshold.length_inches,width_inches,height_inches: optional; without them Navexa uses the default package.
Response
Always HTTP 200; on an error the rates array is empty. The exception is a missing or wrong signature once your account requires signed requests: HTTP 401.
json
{
"rates": [
{
"service_name": "UPS Ground",
"service_code": "UPS_GROUND",
"total_price": 899,
"description": "3-5 business days",
"currency": "USD",
"estimated_days": 4,
"min_delivery_date": "2026-06-05",
"max_delivery_date": "2026-06-09"
}
]
}total_price: the rate in cents.estimated_days,min_delivery_date,max_delivery_date: optional. Business days from the order to delivery, counting the order cutoff, handling days and carrier holidays.