# Medusa Connector

> Two-way Medusa.js integration (v1 and v2): products, variants, customers, orders, refunds and stock, with a webhook companion and sync queue.

- **What it is:** Two-way Medusa.js integration (v1 and v2): products, variants, customers, orders, refunds and stock, with a webhook companion and sync queue.
- **For:** eCommerce
- **Odoo versions:** 16.0, 17.0, 18.0, 19.0, 20.0 - Community and Enterprise
- **Runs on:** Odoo.sh or Odoo you host yourself - not Odoo Online, which accepts no third-party apps
- **Price:** 199 EUR, one-time, on the Odoo Apps Store
- **Technical name:** bambooforge_medusa_connector
- **Run for real:** Tested for real against Medusa v2: 4 products, 3 customers and 3 orders placed through the Store API came into Odoo in 10 sync jobs with 0 errors, and every Odoo total equals the store total.
- **Not for:** The connector reads products, customers, orders and refunds (order_slip) from your Medusa store and writes the corresponding Odoo records. Credentials are stored in admin-only fields.; In scope today: product/variant/customer/order import, product export, order-status mapping, refund discovery, scheduled sync, signed webhooks.; Out of scope / best-effort: tax auto-matching (off by default, name-based, never auto-creates taxes); multi-warehouse stock routing; subscription order types.
- **Manual:** https://docs.bambooforge.dev/bambooforge_medusa_connector/
- **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_medusa_connector.action_medusa_control_tower
- **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_medusa_connector
- **Manual last updated:** 2026-10-15

---
# Medusa Connector

A two-way bridge between Odoo 20 and your Medusa.js store (v1 and v2): import
products with variants, customers and orders, push product changes back, and
reconcile refunds — with a resilient job queue, dry-run safety, validation and
rollback so you stay in control.

This page is the complete manual. If you follow it top to bottom you can install,
connect, run your first sync, automate it, and fix the common issues **without
contacting support**.

## Matching the storefront total

A storefront charges shipping and grants discounts outside the product lines, so
an order rebuilt from those lines alone is worth less than the order the customer
paid for: the sale looks smaller than it was and never reconciles against the
platform payout.

With **Match Storefront Totals** on the instance (on by default):

- the shipping charge is imported as its own order line, on a clearly named
  service product;
- whatever still separates Odoo from the platform total — platform discounts,
  fees, rounding — is posted as a single adjustment line rather than dropped;
- the platform total and the remaining difference are stored on the order and
  shown on its connector tab;
- an order that still does not match within **Total Tolerance** raises a
  validation warning, so the gap is visible instead of silent.

Imported lines deliberately carry no Odoo tax: the storefront has already
computed tax and it is kept on the order's own tax amount field. Switch the
option off if you would rather Odoo recompute everything from your own price
lists and tax rules.

### Overview

The connector keeps Odoo and Medusa in sync over the Medusa **Admin API**, using a
JSON REST client that speaks both the **Medusa v2** and **Medusa v1** lines. A
capability layer absorbs the route and money-unit differences between them (v2 uses
major currency units, v1 uses minor units / cents). It covers:

- **Products** — import Medusa products (with variants, options and images) into
  Odoo product templates, optionally with stock and tax, and export Odoo product
  changes back to the store.
- **Customers** — import Medusa customers and their addresses into Odoo contacts.
- **Orders** — import Medusa orders into Odoo sale orders, mapping each order's
  Medusa status to a sale-order action (keep draft, confirm, cancel, or ignore).
- **Refunds** — optionally discover Medusa `order_slip` credit notes attached to
  an order and create matching draft customer credit notes in Odoo.

Direction of sync: **Odoo ↔ Medusa** (import is the primary flow; product export
and webhook-driven updates are supported).

Every remote call goes through a queue: nothing is written to Odoo until a job runs,
jobs retry with back-off on transient failures, and a circuit breaker pauses an
instance that keeps failing.

### Tested against the real thing

Automated tests run against a bundled mock store on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records and real order totals.

Tested for real against Medusa v2: 4 products, 3 customers and 3 orders placed through the Store API came into Odoo in 10 sync jobs with 0 errors, and every Odoo total equals the store total.

### Requirements

- **Odoo**: 20.0, Community or Enterprise.
- **Medusa**: both **Medusa v2** and **Medusa v1** are supported; pick the matching
  line in the **API version** field. The Medusa server must be reachable from the
  Odoo server.
- **Python**: no extra libraries beyond a standard Odoo 20 install.
- **Odoo access**: any internal user can use the connector screens. Medusa
  credentials are stored in system-only fields, so only the **Settings /
  Administration** user can read or change the API key, password, publishable key
  and webhook secret.
- **Network**: outbound HTTP/HTTPS from Odoo to your Medusa server. By default the
  connector refuses internal/loopback/private hosts as an SSRF safeguard (see
  *Safety features*).

### Installation

1. Copy `bambooforge_medusa_connector` into your Odoo addons path.
2. Restart the Odoo service.
3. Open **Apps**, click **Update Apps List**, search for *Medusa*, and press
   **Activate / Install**. Dependencies (*Sales*, *Contacts*, *Invoicing*,
   *Product*) install automatically.

No Medusa store is required to evaluate the connector: a mock Medusa Admin API
ships inside the module, so you can install, explore and run the full import flow
against sample data before pointing it at a real store. Point the instance **Base
URL** at `<your-odoo>/medusa/mock_api` with auth type *Email + Password* (any
email/password is accepted) to try it.

## Webhook subscriber companion (optional, for real-time sync)

To push changes from Medusa to Odoo in real time, install the small webhook
subscriber that ships with this module at
`phase2/medusa_subscriber/bambooforge-webhook.ts`:

1. Copy `bambooforge-webhook.ts` into your Medusa project's `src/subscribers/`
   directory.
2. Set three environment variables in your Medusa `.env`:
   
   - `BAMBOOFORGE_WEBHOOK_URL` — e.g. `https://odoo.example.com/medusa/webhook`
   - `BAMBOOFORGE_WEBHOOK_SECRET` — must match the instance **Webhook Secret** in Odoo
   - `BAMBOOFORGE_INSTANCE_ID` — the `medusa.instance` id in Odoo (optional but
     recommended; appended as `?instance_id=`)
3. Restart Medusa. The companion is written for the Medusa v2 subscriber API; for
   Medusa v1 see the README in that folder.

Without the companion, the scheduled reconciliation still keeps data current (see
*Automation*).

### Step 1 — Get Medusa Admin API access

The connector authenticates to the Medusa **Admin API**. Choose one of three auth
types on the Odoo instance; how you obtain the credential depends on your Medusa
line:

**Medusa v2 — Admin Secret API Key (recommended)**

1. Sign in to the Medusa Admin dashboard as an admin user.
2. Go to **Settings ▸ Secret API Keys** (Developer / API key management).
3. Create a **Secret API Key**, give it a name like `Odoo`, and copy the key
   once it is shown.
4. In Odoo, set **Authentication** to *Admin Secret API Key (v2)* and paste the key
   into **API Key**. The connector sends it as HTTP Basic (key as username, empty
   password).

**Medusa v2 — Email + Password (JWT)**

If you prefer not to manage a secret key, set **Authentication** to *Email +
Password (JWT)* and enter an admin **Email** and **Password**. The connector
exchanges them for a JWT at `POST /auth/user/emailpass` and caches the token.

**Medusa v1 — Email + Password or pre-issued token**

For a Medusa v1 server, set **API version** to *Medusa v1*. Either use *Email +
Password (JWT)* (the connector logs in at `POST /admin/auth/token`), or, if you
already hold a token, set **Authentication** to *Bearer Token* and paste it into
**API Key**.

Optional: if your Medusa requires a publishable key for store-scoped calls, paste
it into **Publishable API Key**; it is sent as the `x-publishable-api-key` header.

### Step 2 — Create the connection in Odoo

Open **Medusa Connector ▸ Configuration ▸ Instances** and create a record.

![Medusa instance / connection cockpit](01-connection-cockpit.png)

Key fields:

| Field | What to enter |
|---|---|
| Name | A label for this store, e.g. *My Live Store*. |
| Base URL | Your Medusa server root, e.g. `https://store.example.com` or `http://localhost:9000`. |
| API version | *Medusa v2* (default) or *Medusa v1*. Drives money units and the auth login route. |
| Authentication | *Admin Secret API Key (v2)*, *Bearer Token*, or *Email + Password (JWT)* — see Step 1. |
| API Key | The secret API key (secret-key auth) or pre-issued token (bearer auth). Leave empty for the email/password flow. Visible to administrators only. |
| Email / Password | Admin email and password for the email/password JWT flow (password is admin-only). |
| Publishable API Key | Optional store-API publishable key, sent as `x-publishable-api-key` (admin-only). |
| Admin API path | Leave the default `/admin` unless your server differs. |
| Default currency code | Currency (e.g. `usd`, `eur`) used to pick a variant price on import. Default `usd`. |
| Verify SSL | Keep on for production. Turn off only for self-signed test certificates. |
| Allow internal host | Off by default. Turn on only to reach a Medusa server on localhost or a private network (lowers the SSRF guard — see *Safety features*). |

Then click **Test Connection**. A green *Connected* state means the credentials and
URL are correct. On the first successful connection the default order-status
mapping is seeded automatically. If it fails, the exact error is shown on the form
and recorded in **Logs** (see *Troubleshooting*).

Tip: use **Quick Setup** (button on the instance) to pick a solution pack (catalog
only, B2C full sync, B2B order-first, retail starter), seed default field mappings
and flows, and apply a recommended safety profile in one step.

### Step 3 — First sync (dry-run, then live)

New instances start with **Dry-run** ON. In dry-run, import and delete jobs
*simulate* writes: instead of changing data they produce **Validation Results** you
can review under **Medusa Connector ▸ Operations ▸ Validation Results**. This lets
you confirm what *would* happen before anything is written.

To run a first import:

1. On the instance, click **Import Products** (and/or **Import Customers**,
   **Import Orders**). This enqueues jobs; it does not block the UI.
2. Jobs are processed by the **Medusa Queue Processor** scheduled action (every
   minute), or immediately if you run it manually from **Operations ▸ Queue Jobs**.
3. Review **Validation Results** while still in dry-run.
4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports
   again to write the records for real.

Imported records land in the standard Odoo apps: **Sales ▸ Products**,
**Contacts**, and **Sales ▸ Orders**. Each carries its Medusa reference (a binder
record) so re-imports update the same record instead of duplicating it.

### Order status mapping

Medusa exposes each order's `status` (e.g. `pending`, `completed`,
`archived`, `canceled`, `requires_action`) and a `fulfillment_status` (e.g.
`not_fulfilled`, `fulfilled`, `shipped`, `returned`) used as a fallback.
Under **Configuration ▸ Order State Mappings** each instance gets a default table
(seeded on first successful connection) that decides what happens to the Odoo sale
order when an order arrives in a given status:

| Medusa status | Type | Default Odoo action |
|---|---|---|
| pending | order status | Keep draft |
| requires_action | order status | Keep draft |
| completed | order status | Confirm sale order |
| archived | order status | Confirm sale order |
| canceled | order status | Cancel sale order |
| not_fulfilled | fulfillment status | Keep draft |
| fulfilled | fulfillment status | Confirm sale order |
| partially_fulfilled | fulfillment status | Confirm sale order |
| shipped | fulfillment status | Confirm sale order |
| partially_shipped | fulfillment status | Confirm sale order |
| returned | fulfillment status | Cancel sale order |

Change any row to **Ignore (do nothing)**, **Keep draft**, **Confirm sale order**
or **Cancel sale order**. Unknown/custom statuses default to *Ignore*, so a status
you have not mapped never triggers a destructive transition. Add a row for any
custom status your store introduces (use the exact Medusa status string).

### Field mapping & customization

- **Field Mappings** (Configuration) map Medusa fields to Odoo fields per model.
  Use **Generate suggested mappings** on the instance to seed the business-critical
  ones, then adjust. The mapping board flags fields that need attention.
- **Schema Fields** lists the discovered Medusa fields per resource. Run **Schema
  Introspection** on the instance to refresh it from your live store (it falls back
  to the bundled mock sample if the store is unreachable).
- **Stock / images / tax / refunds** are opt-in toggles on the instance: *Sync
  stock*, *Sync images* (and *max images*), *Upload images on export*, *Sync
  taxes* / *Auto-match taxes on import*, *Sync refunds* / *Auto-post refund*. They
  are off by default; turn on only what you need. Tax auto-matching is name-based,
  company-scoped, and never auto-creates taxes.

### Automation (scheduled actions & webhooks)

The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled
Actions**):

| Scheduled action | Default | Purpose |
|---|---|---|
| Medusa Queue Processor | every 1 min | Processes queued import/export/delete jobs. |
| Medusa Reconciliation | every 15 min | Pulls recent remote changes for enabled models. |
| Medusa Maintenance | every 1 hr | Recovers stale/locked jobs and trims old logs. |
| Medusa Flow Scheduler | every 5 min | Runs scheduled sync flows. |
| Medusa Flow Metrics | every 1 hr | Aggregates flow-run metrics. |
| Medusa Auto Recover | every 15 min | Reopens a tripped circuit breaker once the store is healthy. |

Turn on **Auto import** / **Auto reconcile** per model on the instance to let the
scheduled actions keep things in sync hands-free.

**Real-time webhooks** (optional): install the subscriber companion (see
*Installation*). It POSTs HMAC-SHA256-signed events to:

- **Delivery URL**: `https://<your-odoo-domain>/medusa/webhook`
  (optionally `?instance_id=<id>` to target a specific instance).
- **Secret**: the instance **Webhook Secret** (admin-only field on the instance).

Each delivery carries an `X-Medusa-Signature` header. Odoo verifies the
HMAC-SHA256 signature over the raw body, rejects stale or replayed deliveries, and
enqueues a safe import. Without webhooks the scheduled reconciliation still keeps
data current.

### Safety features

- **Dry-run mode** — simulate writes and review Validation Results before going
  live (field *Dry-run mode*, ON by default).
- **Business validation profiles** — *Minimal / Standard / Strict* gate risky
  writes (field *Business validation profile*).
- **Safety profile** — *Conservative / Balanced / Aggressive* presets set the
  dry-run, validation, batch-size and webhook dials in one click.
- **Resilient queue** — every remote action is a job with retry, exponential
  back-off and a dead-letter state.
- **Circuit breaker** — after repeated failures an instance auto-pauses
  (*Tripped*); the Auto Recover action reopens it once the store responds again
  (auth/configuration failures trip it immediately and require manual resume).
- **Rollback snapshots** — when *Rollback enabled* is on, imports capture a
  snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots**.
- **SSRF guard** — the connector refuses internal/loopback/private hosts unless
  *Allow internal host* is explicitly enabled.
- **Signed webhooks** — the public webhook route fails closed: a missing secret or
  signature is rejected, and stale/replayed deliveries are dropped.

### Where Medusa shows up in Odoo

The connector stays out of the way on records that have nothing to do
with Medusa:

- **Sales orders** - the *Medusa* tab, *Queue Medusa Export* and *Medusa Binding*
  appear only on orders that came from Medusa, are bound to Medusa, or belong
  to a customer bound to Medusa.
- **Products and contacts** - the *Medusa* tab appears once an active Medusa
  connection exists; the *Medusa Binding* button inside it appears once the
  record is bound.
- List views, the sync queue and the control tower are unchanged.

### Troubleshooting

| Symptom | Cause and fix |
|---|---|
| Test Connection fails with 401 / unauthorized | Wrong or revoked API key, token, or email/password; or the wrong **API version** for your server. Re-check Step 1 and that **Authentication** matches your Medusa line. |
| "An API key / token is required" error | The selected auth type needs **API Key** filled (secret-key or bearer). Either fill it or switch **Authentication** to *Email + Password (JWT)*. |
| "Email and password are required" | Auth type is *Email + Password* but one of the fields is blank. Fill both, or switch to a key/token auth type. |
| "Base URL ... is not allowed" / refused internal host | Base URL points at localhost/private IP. Enable *Allow internal host* on the instance (trusted self-hosted Medusa only). |
| SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off *Verify SSL* for testing only. |
| Orders import but never confirm | The order's Medusa status is mapped to *Keep draft* or *Ignore*. Adjust **Order State Mappings**. |
| Product prices are wrong / zero | The variant has no price in **Default currency code**. Set the matching currency (e.g. `usd`, `eur`) on the instance, or check the v1/v2 **API version** (v1 uses minor units / cents). |
| Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check the **Medusa Queue Processor** scheduled action is active and the instance is not *Paused*. |
| Instance shows *Tripped* | Circuit breaker tripped after repeated failures. Fix the store/credentials; Auto Recover reopens it after cooldown, or click **Resume**. |
| Webhook returns 401 (Invalid signature) | The Medusa `BAMBOOFORGE_WEBHOOK_SECRET` does not match the instance **Webhook Secret**, or the signature header is missing. Align both. |
| Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. |

For anything else, **Operations ▸ Logs** records every API call and error with a
timestamp.

### Frequently asked questions

**Which versions are supported?**
Odoo 20.0 on the Odoo side. On the store side both **Medusa v2** and **Medusa v1**
are supported — pick the line in the **API version** field. Validate your exact
build with the bundled mock first.

**Do I need a Medusa store to evaluate it?**
No. A mock Medusa Admin API ships inside. Point the Base URL at
`<your-odoo>/medusa/mock_api` (auth *Email + Password*, any credentials) to run
the full import flow before connecting a real store.

**How does authentication work?**
Three ways: a Medusa v2 **Admin Secret API Key** (sent as HTTP Basic), a
pre-issued **Bearer Token**, or **Email + Password** exchanged for a JWT
(`/auth/user/emailpass` on v2, `/admin/auth/token` on v1). All three are built
in.

**How does real-time sync work?**
Install the bundled webhook subscriber companion in your Medusa project. It POSTs
HMAC-SHA256-signed events to `/medusa/webhook`; Odoo verifies the signature and
enqueues a safe import. Without it, scheduled reconciliation keeps things in sync.

**Is it safe to run against production data?**
Dry-run is ON by default, validation blocks risky writes, and rollback snapshots
let you undo. You decide when to go live.

**What support and refund policy do I get?**
Every request is answered within 24 hours, setup help included. If you report a bug
within 2 months of purchase and it is not resolved within 15 days, you are entitled
to a full money-back refund.

### Data, privacy & limits

- The connector reads products, customers, orders and refunds (`order_slip`) from
  your Medusa store and writes the corresponding Odoo records. Credentials are
  stored in admin-only fields.
- **In scope today**: product/variant/customer/order import, product export,
  order-status mapping, refund discovery, scheduled sync, signed webhooks.
- **Out of scope / best-effort**: tax auto-matching (off by default, name-based,
  never auto-creates taxes); multi-warehouse stock routing; subscription order
  types.

### Support & updates

- Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help
  included.
- Refund: report a bug within 2 months of purchase; if unresolved within 15 days,
  full refund.
- Full source is included. Updates track the supported Odoo 20 / Medusa v1 & v2
  Admin API line.

### Upgrading & version compatibility

This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its
own dedicated build of this module — always install the build that matches your Odoo
version. Mixing a build with a different Odoo series is not supported.

## Patch upgrades (same series, e.g. 20.0.1.0.0 → later)

1. **Back up** your database and filestore first.
2. Replace the module folder with the newer build.
3. Restart Odoo with the module updated:
   
   ```
   ./odoo-bin -c your.conf -u bambooforge_medusa_connector -d your_db
   ```
4. Odoo applies any schema/data changes automatically. Your existing records and
   configuration are preserved.

## Cross-version migration (moving to a newer Odoo)

Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade
tooling. When you migrate the database to the next Odoo series, install the matching
build of this module for that series. Data created by this module carries over with
the database migration.

After any upgrade the module's scheduled actions resume on their normal cadence —
no manual re-activation is required.

### Uninstallation

You can remove this module at any time from **Apps → (this module) → Uninstall**,
or from the command line. Uninstalling is clean and reversible by reinstalling —
but note what is and is not deleted.

## What is removed

- The module's own tables and every record in them (20 models, prefixed `medusa.*`) — this is the data this module created.
- The menus, actions, views and reports this module installed.
- Its scheduled actions (cron jobs) — they stop immediately on uninstall.
- Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo.

## What is preserved

- **Your remote platform is never touched.** Uninstalling only removes the Odoo-side
  connector; products, customers and orders on the external store/service are untouched.
- Records already imported into standard Odoo models (e.g. contacts, products, sales
  orders) remain — they are ordinary Odoo records once created.
- Attachments and chatter messages on standard records are kept.

As always, take a database backup before uninstalling in production.

### Changelog

## 20.0.1.2.5

- Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships.

## 20.0.1.0.0

Current release for Odoo 20.0. This build includes:

- Two-way Odoo 20 <-> Medusa.js (v1 & v2) connector: products, variants, customers, orders and refunds.
- JSON REST client, webhook subscriber companion, two-way stock sync, live Sync Control Tower, resilient queue and dry-run safety.

Feature additions and fixes ship as new builds on the Odoo Apps store; this page and
the module's `version` reflect the current published release. Always keep the build
matched to your Odoo series (see *Upgrading & version compatibility*).

### How we make sure it works

Before every release, **105 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20.
Among them:

- A domain that does not parse is refused.
- Nothing at all is an empty row not a crash.
- A product shows its binding button only once bound.
- A record outside the filter is not queued.
- Map refund to credit note.

Tested for real against Medusa v2: 4 products, 3 customers and 3 orders placed through the Store API came into Odoo in 10 sync jobs with 0 errors, and every Odoo total equals the store total.

The complete list is in the source you receive.
