BaseLinker by Koda Plus: Medusa and BaseLinker (Base) in both directions. Links or creates cards, imports BaseLinker products, carries stock and prices either way, sends store orders exactly once, imports Allegro, Amazon and other marketplace orders exact
Connect Medusa to BaseLinker (Base, base.com), the order and warehouse hub most Polish online stores run their marketplaces through, in both directions. You decide where the catalog lives and whose stock is the truth, and the plugin carries the rest: it links Medusa variants to BaseLinker cards or creates the missing cards, imports BaseLinker products when the catalog lives there, carries stock and prices the way you chose, sends store orders to BaseLinker exactly once, imports Allegro, Amazon and other marketplace orders into Medusa exactly once, and keeps statuses, parcels, returns and invoice numbers in step.
Every write that changes data ships switched off. It waits behind a plan your team reads item by item, and behind a writer a person arms in the admin, with their name and the date next to it.
Live demo: Medusa admin medusa.koda.plus/app/baselinker, signed in to a public demo account by the link itself, and the storefront demo.koda.plus. The demo runs the plugin in demo mode ( with ), so every screen has data: place an order in the storefront and watch its BaseLinker row reach "Wysłane" with a tracking number a few minutes later, read the plans, arm a writer, and watch simulated Allegro and Amazon orders become Medusa orders. The Setup guide view () walks through a real setup step by step.
What it syncs
Card links (BaseLinker card to Medusa variant, by SKU, then EAN): read only, every hour at minute 15 and on Sync cards now. Variant cards are read too; a main card that has variants is a container and is never linked itself.
Catalog from BaseLinker (with ): products and variants with the price of one price group, SKU, EAN, weight, images, description, category and manufacturer, into Medusa after every complete read. Planned; applied by the writer.
Cards from Medusa (with , the default): a new BaseLinker card for each variant that has none (name, SKU, EAN, weight, description, images, and the price and stock where they are known), and the name and EAN of linked cards kept in step. Planned; applied by the writer.
Stock, BaseLinker to Medusa (with , the default): one BaseLinker warehouse into Medusa inventory levels. Planned; applied by the writer.
Stock, Medusa to BaseLinker (with ): what Medusa can still sell, into one warehouse. Planned; applied by the writer.
Prices, Medusa to BaseLinker (catalog in Medusa and set): the Medusa price in , gross, into the price group. Planned; applied by the writer. With the catalog in BaseLinker, prices come in with the catalog import instead.
Store orders (lines, prices, shipping address, invoice data, payment and cash on delivery flags): Medusa to BaseLinker on , then every 2 minutes for retries, and on Send to BaseLinker now.
Marketplace orders (the BaseLinker order sources you choose: Allegro, Amazon, eBay, Erli and the rest): BaseLinker to Medusa, found every 5 minutes, created by the writer, exactly once each.
Order status and tracking (status name, parcel number, carrier, tracking link): BaseLinker to Medusa order metadata, for sent and for imported orders, every 15 minutes (from the BaseLinker order journal when it is enabled) and on Read statuses.
Fulfillment and cancellation: the Medusa fulfillment of the remaining items when BaseLinker reaches one of (imported orders need for that); an imported order is cancelled in Medusa when BaseLinker reaches one of and nothing is fulfilled yet, and flagged for a person otherwise. A payment that arrives later in BaseLinker marks the imported order paid.
Returns: the BaseLinker return manager, read only, every hour at minute 45 and on Read returns, linked to the Medusa order when the order is one this plugin sent or imported.
Invoice numbers: the number of each invoice or receipt the Fakturownia plugin of Koda Plus issues, into one field of the BaseLinker order. Applied by the writer.
Write safety
Off until a person arms it. Seven writers: , , , , , and . Each has two switches. The option is a hard switch: it wins and cannot be overridden from the admin. The arm switch in the admin is stored in the database with who flipped it and when. Both stock writers also need .
Write barrier by method name. Every method passes. passes only while is on. , , and pass only with the permit of the writer that owns them, which a run gets only while that writer is armed. Every other method (, , , and the rest) is blocked by name before a request leaves the process.
Plan first. Catalog, card, stock and price changes are computed as a plan (what changes, from what to what), shown in the admin, and applied only by an armed writer, up to a cap per run. An item that fails runs in a row (3 by default) is quarantined until a person releases it. An incomplete read plans nothing, and an item missing from a read is never zeroed or deleted.
Exactly once. Store orders, marketplace orders and invoice numbers each get a unique row before anything goes to the network and a lease, so a crashed process never leaves a row stuck. One run of each job and one worker per order, marketplace order and invoice document hold across every process of the store (a server and a worker, or several instances), as "Running more than one process" below explains. Every create is preceded by a lookup: the marker in BaseLinker for store orders, and in Medusa for marketplace orders, the SKU in BaseLinker for new cards, the current value of the order field for invoice numbers. An unclear answer is settled by another lookup, never by a blind retry.
Retries only for transient errors (network, timeouts, 5xx responses, the rate limit), with backoff. A refusal about the item itself counts towards its quarantine; an outage does not.
Features
Admin page with two views. The Panel: connection check with everything the account offers (catalogs, warehouses and which of them take stock, price groups, order sources, statuses, custom order fields, journal state), the directions and writers, the plan of every direction in force with its filters and quarantine, the cards, the store orders, the marketplace orders, the returns, the invoice numbers and the history of runs. The Setup guide: a diagram of the parts, eleven steps from the token to go-live with a live state each, a go-live checklist ticked from the store itself, and answers to the failures this integration really meets.
Order widget on every order: the BaseLinker order, its status and the tracking link with Send to BaseLinker now, or why the order stays out; for an imported marketplace order its source, payment state and parcel instead. Product widget on every product: the linked cards, their BaseLinker stock and the main card they hang under. Both can be embedded by an app that hosts every Koda Plus plugin in one card (see "Works with Koda Plus hosts").
Deep links: the page opens on one list from a link, for example , or .
References ("Running in production"): stores you pass in the option are shown on the Panel and at the end of the guide.
Demo mode: a simulated BaseLinker account built from your own catalog, with duplicated SKUs and EANs, price groups, both stock directions, Allegro and Amazon orders and returns. Nothing leaves Medusa.
Admin in English and Polish.
Workflows and events for your own code.
Requirements
Medusa 2.12 to 2.21 and Node.js 20+ (see "Compatibility").
For a real account: a BaseLinker API token (in BaseLinker: Account & other, My account, API) and a catalog (inventory) with a warehouse. The plugin works with catalogs, not with BaseLinker's old storage.
For marketplace orders: a Medusa region for the order currency and a sales channel whose stock location holds the stock. For invoice numbers: the Fakturownia plugin of Koda Plus (optional, never imported as a package).
Installation
Yarn and pnpm work the same way: or .
Configuration
Add the plugin to :
Run the migrations, then open BaseLinker in the admin sidebar and click Check connection:
npx medusa db:migrate
Setup in brief
The Setup guide view of the admin page has the same steps, each with its live state.
Create the API token. In BaseLinker open Account & other, My account, API and generate a token for this store. Put it in .
Configure the plugin as above and run . Missing values never break the start: the Panel lists what is missing.
Choose the catalog and the warehouse.Check connection lists the catalogs with their warehouses. Set , and a warehouse of type as : only warehouses take stock writes, and ones mirror an external system.
Price group and order statuses. Check connection lists the price groups and the order statuses. Set (a standard group in your currency, not a derived one), for new orders, for shipped, for finished and for cancelled.
A custom order source (recommended). In BaseLinker open Orders, Settings, Custom order sources, create one named after the store and set its id as .
Decide the source of truth for each kind of data: the catalog (), stock (), prices (they follow the catalog) and marketplace orders (). Clear the duplicated SKUs and EANs the cards show before any import or push.
Read the plans.Sync cards now reads BaseLinker and makes the plan of every direction in force. Nothing is written.
Arm the writers one at a time, reading the history after each. Start the cards writer with and look at that one card in BaseLinker.
Marketplace orders (optional). Set , make the store's confirmation e-mail skip orders with , then arm the order import writer.
Invoice numbers (optional). Choose , then arm the invoice number writer.
Go live with the checklist of the guide.
Options
Every option is optional. Numbers may come as strings (environment variables), lists as comma separated strings. Missing options never break the boot: the module registers, the admin lists what is missing and each job waits for the options it needs. Demo mode never switches itself on: it runs only with an explicit (the live demo sets ). Without a token and without demo mode the plugin says "Not configured" and sends nothing.
Order export starts the moment is on (the default) and is set, before any writer is armed: every new order goes to BaseLinker from then on, test orders of the setup included. Keep until the store goes live, or mark test orders with the skip key.
Connection
: BaseLinker API token. Stays on the server and is masked in every log, stored error and admin screen.
: the BaseLinker catalog (inventory) the cards live in.
: the warehouse whose stock counts, like (a bare number becomes ). Stock pushed from Medusa needs a warehouse.
: the BaseLinker status new store orders get.
(optional): a custom order source created in BaseLinker, so Medusa orders are labeled there. It also lets the status read fetch 100 orders per request, and keeps the marketplace import from taking our own orders back.
(optional when the store has one location): the Medusa stock location stock is read from and written to.
(default ): the simulated BaseLinker account. Only an explicit (or ) switches it on.
(default ): demo mode only. Simulated marketplace orders become Medusa orders of the store (flagged ) once the order import writer is armed. Off, they stay rows of the import list. Meant for a demo store like medusa.koda.plus, never for a store that sells.
(default , at most ): self-imposed rate limit shared by every job.
(default ): one BaseLinker request.
Directions, writers and limits
(default ): links cards and lets the writer create the missing ones; lets the writer import BaseLinker products into Medusa. Prices follow the catalog.
(default ): plans BaseLinker stock into Medusa; plans Medusa stock into the warehouse.
(default ): (no stock plan at all), (plans are stored and shown, nothing is written) or (the stock writer of the direction may be armed). Version 0.1 configurations with keep writing BaseLinker stock into Medusa until a person touches the arm switch.
(default: none switched off): hard switches by writer name, for example . (or ) wins over the admin.
(default none): the BaseLinker price group the catalog import reads and the price push writes.
(default ): the Medusa currency of that price group.
(default ): imported products and created or updated cards per run.
(default ): stock changes per run, in either direction; decreases go first.
(default ): price changes per run.
(default ): failed runs of one item before it is quarantined.
(default ): the hourly read and every plan after it.
Catalog import ()
(default ): status of the products the import creates, or .
(default: the store's default sales channel): sales channel of imported products.
(default: the default shipping profile): shipping profile of imported products.
(default ): the option of imported products with several variants; its values are the variant names.
(default ): create Medusa categories that do not exist yet (matched by name). Off, a missing category is only reported.
(default ): the manufacturer goes to , or to a product tag with (only the tag naming a BaseLinker manufacturer is ever replaced).
(default ): an imported product removed in BaseLinker becomes a draft. Off, it is only reported. Nothing is ever deleted.
(default ): the unit of Medusa weights, or . BaseLinker uses kilograms.
Store orders to BaseLinker
(default ): send placed orders to BaseLinker.
(default none): BaseLinker status ids that create the Medusa fulfillment for the remaining items.
(default none): BaseLinker status ids after which an order is no longer read. Without them, orders are followed for 30 days.
(default , ): payment provider id prefixes meaning cash on delivery.
(default: built-in English names): payment method names shown in BaseLinker by provider id prefix, for example . Up to 30 characters each.
(default ): an order with never goes to BaseLinker. Use it for test orders.
(default ): orders another plugin took straight from a marketplace (they carry ) go to BaseLinker too. Off by default, because BaseLinker usually gets them from its own marketplace integration.
Marketplace orders into Medusa
(default none, nothing is imported): BaseLinker order sources to import, as types or , for example or . The ids come from Check connection.
(default: the region of the order currency): region of imported orders.
(default: the store's default sales channel): sales channel of imported orders.
(optional): shipping option of imported orders. With it, fulfills them too.
(default none): BaseLinker statuses that cancel the imported Medusa order while nothing is fulfilled.
(default: 24 hours before the first discovery): ISO date of the first orders to read.
(default ): an order older than this when the writer reaches it waits for a person instead of being imported.
Returns, journal, invoice numbers, references
(default ): read returns, read only.
(default ): returns of the last N days.
(default ): reads statuses from the BaseLinker order journal when it returns events, with a full read every 2 hours and after a gap of more than 2 days; always reads the full way.
(default ): the BaseLinker order field for invoice numbers: , or a custom order field id ( or ).
(default , ): Fakturownia document kinds whose number is written.
(default none): stores running this integration, shown as "Running in production": , texts as strings or . marks a store that starts on Medusa soon: it is shown with a "Soon" badge and no link, and its is optional. Entries without a name, or live entries without an https URL, are dropped, never an error.
How linking works
The whole catalog is read with and , 1 000 cards per page, from page 1 until a short page. A failed page or the page ceiling makes the read incomplete.
A main card that has variants is a container: stored and shown, never linked. Its variant cards are linked like simple cards.
Every card is looked up by SKU, uppercased and trimmed on both sides. A SKU that sits on two or more cards is a conflict; two variants with one SKU are an conflict. Neither is linked.
A card the SKU did not settle is looked up by EAN (digits only, 8 to 14), with the same rules (). A variant is linked to at most one card, and a SKU link wins over an EAN link.
A complete read replaces the snapshot. An incomplete read only adds and updates: a card missing from a broken list looks exactly like a deleted one, so nothing is unlinked.
Variants with a SKU that no card carries are counted as "only in Medusa".
Why so strict: on a production account we measured 12 993 cards carrying 8 522 distinct SKUs, and at a tyre retailer with about 11 000 cards, 4 438 SKU pairs were the same physical item entered twice. Linking "the first one" would send stock and orders to a random copy.
Catalog import (plan first)
With , every complete read is followed by (100 cards per request), and , and a plan:
Create: a BaseLinker product with no Medusa counterpart becomes a Medusa product with its variants (one option, ), the price of , SKU, EAN, weight, images in BaseLinker's order, description, category (by name) and manufacturer, and . The new variants are linked to their cards at once.
Update: a linked product whose title, description, images, category, manufacturer, variant title, price, EAN or weight differs is updated field by field; unchanged fields stay out of the write. A new BaseLinker variant of an imported product adds an option value and a variant.
Draft: an imported product removed in BaseLinker, only with .
Skip or conflict, with the reason: bundles, cards without a SKU, duplicated SKUs or EANs, a SKU on two Medusa variants, variants spread over two Medusa products, a new variant of a product the import did not create, a missing category while is off.
The import is idempotent by the BaseLinker id: a product it created is found by and its variants by their card links, so a second run updates instead of creating again. Writes go through Medusa's own workflows ( in batches of 10, , ) and prices through the Pricing module, which adds or replaces only the base price of and leaves every other price alone. BaseLinker prices are gross: for net Medusa prices they are divided by the product's VAT rate, and a product without a rate gets no price change at all.
Cards from Medusa (plan first)
With , every complete read is followed by the card plan:
Create a card for a variant that has none: name (product and variant title, up to 200 characters), SKU, EAN, weight in kilograms, description, up to 16 images, the price in when Medusa prices include tax, and the first stock when Medusa is the source of stock. Each variant becomes its own card; grouping cards under a main card stays a decision made in BaseLinker.
Update a linked card whose name or EAN no longer matches Medusa. Only those two fields are sent: prices and stock have their own plans.
Never a second card for the same goods. A variant whose SKU already sits on any card, or whose EAN sits on a card with another SKU, is a conflict. Before every create the plugin looks the SKU up in BaseLinker, and after an unclear answer it looks again instead of creating twice. A SKU longer than the 50 characters BaseLinker keeps is skipped, never sent cut.
The created card is linked to its variant at once, so the next plans (stock, prices) already see it.
Stock in both directions (plan first)
The plan covers linked, conflict-free variants that manage inventory with exactly one inventory item, after every complete read. Kits are skipped, and so is everything after an incomplete read.
BaseLinker to Medusa (): . BaseLinker reports what can still be sold while Medusa keeps before its reservations; adding the reservations back makes Medusa's available quantity equal to BaseLinker's, and an order already sent to BaseLinker does not take the item twice. Applied with Medusa's own .
Medusa to BaseLinker (): of the Medusa level, written with into the warehouse, 1 000 cards per request. A level that does not exist is unknown, never zero. A card BaseLinker refuses (its warning names the card) fails alone and counts towards its quarantine.
Both directions write absolute numbers, never deltas, so a repeated write is harmless; decreases go first and caps a run. Why a plan first: BaseLinker numbers are not always the truth. On a production account we found duplicated cards with different stock, negative stock after overselling and a nightly import that kept raising zeros back.
Prices (plan first)
With the catalog in Medusa and set, the base Medusa price in of every linked variant is compared with the card's price in that group and changed ones are written with , 1 000 per request, within . BaseLinker keeps gross prices: net Medusa prices are multiplied by the card's VAT rate, and a card without a rate is left alone. Before the first write of a run the plugin reads the price groups and writes nothing into a group derived from another one (BaseLinker computes those itself), a group the account does not have, or a group in another currency than ; Check connection shows the same warnings.
Store orders exactly once
BaseLinker has no idempotency key, and a mapping row written after cannot protect against a lost answer. So:
writes a row in first, then sends in the background. The payload is built from the order at send time and never stored, because it carries personal data.
Every attempt scans (from the order date minus one hour, unconfirmed orders included, paging by ) for the marker . Found: that BaseLinker order is adopted and nothing is written.
Otherwise one . It is never retried blindly; only a rate limit refusal repeats, because then BaseLinker took nothing.
A timeout, a 502 or broken JSON after means "no answer", not "no order": the plugin waits and scans again. Still not found, the row is retried later, and that attempt scans first.
Success writes , the BaseLinker order id and , and emits (live mode; the demo writes its own row only). After the backoff runs out (about two and a half days) the row is , is emitted, and a person can Send again, which is always safe.
Lines whose variant is linked go to their card (, the catalog id, the card id), so stock moves on the right card in BaseLinker. Other lines go as free lines with name, SKU and EAN; the order still arrives. The unit price is the line total after discounts divided by the quantity, the tax rate is the highest rate of the line. is 1 only when the payment is captured in full; marks cash on delivery by payment provider. Invoice fields go only when the buyer asked for one ( or a tax id such as ), and InPost lockers from the shipping method data become the delivery point.
Orders that came from a marketplace never go back: an order this plugin imported (its import table says so, never the order metadata) gets no row, and an order another plugin created with is skipped unless is on. Every skip is a visible row with a code: , , or .
If Medusa cannot compute an order's totals (a shipping method without a version, as some orders created by other plugins have), the order is read again without them and its totals are computed from the lines: unit price times quantity minus adjustments, with the tax of the tax lines on net prices. says so, and is 1 only when the captured money covers both that sum and the payment collection. When even that read fails, the row waits with and tries again.
Marketplace orders into Medusa (exactly once)
Discovery runs every 5 minutes while is set: from a cursor on (confirmed orders only, 100 per page), filtered to the chosen sources. Our own exports (the marker in ) and orders of our custom source are never taken. Each order becomes a row, one per BaseLinker order id: the admin shows them before anything is created. The cursor starts again at the same second and skips the ids it has seen, so orders confirmed in the same second as a page boundary are not lost.
Import runs only while the writer is armed, per row, under a lock on the BaseLinker id:
The row's next attempt moves ten minutes ahead first, so a crashed process leaves a row that comes back by itself.
An order older than waits for a person (Import now imports it anyway).
The order is read again from BaseLinker. The buyer's data goes onto the Medusa order and nowhere else.
Lookups before create: a Medusa order this row created earlier (it carries the row's id in , or it has no cart behind it) is adopted. A Medusa order created by backend code with the same (taken by another plugin or by hand) makes this one . An order placed from a storefront cart counts for neither: its metadata is the shopper's word. The reference is looked up again inside a lock named , the key the other marketplace plugins of Koda Plus take for the same order, so the one that comes second sees the first one's order.
The order is created as a draft with Medusa's own and its id is stored at once, then placed with , which reserves the inventory and emits , just as the admin does with draft orders. Lines of linked cards get their variant, other lines stay custom lines; prices come from the order (gross, ), taxes from the region, delivery becomes a shipping method with its price, a pickup point goes to the shipping method data, and the e-mail is set on the order itself, never through a guest customer.
A payment collection for the total, marked paid through Medusa's when BaseLinker has the order paid in full. Cash on delivery and unpaid orders stay unpaid until BaseLinker reports the payment.
Why and not the cart: Medusa documents it for importing orders from an external system, it takes our own unit prices, checks the inventory, computes tax lines from the region and keeps the shipping method we give it. The cart path would need a payment session and a shipping option the marketplace order never had.
Imported orders carry , , , and (lowercased, for Allegro ), plus . is emitted for them on purpose (invoices, ERP documents and stock follow-ups should see them), so make your order confirmation e-mail skip orders with : the marketplace talks to its buyer.
Statuses and the order journal
Every 15 minutes the plugin reads the status, parcel number and carrier of the orders it sent and of the orders it imported, writes them into its rows and into order metadata and creates fulfillments or cancellations as described above. The 60 orders read in one pass are chosen by the database: never checked first, then the oldest check, closed statuses left out. is emitted for sent and for imported orders (). With it first asks for order events since the last one it processed; only the orders with events are read. The journal keeps three days and has to be enabled for the account (Account & other, My account, API, or Base support), so the plugin also reads everything the full way every 2 hours, after a gap of more than 2 days, and for as long as the journal has never returned an event.
Returns (read only)
Every hour the plugin reads for the last days with the return statuses and reasons, and shows each return with its products, status, reason, refunded amount and parcel, linked to the Medusa order when the order is one it sent or imported. Buyer data (e-mail, phone, address, bank account, the buyer's comment) is dropped before anything is stored. Nothing is written anywhere.
Invoice numbers from Fakturownia
When the Fakturownia plugin of Koda Plus emits for an order that is in BaseLinker (sent or imported), the number goes into the order field with , exactly once per document. The field is read first: empty means write, the same number means it is already there, any other value is a conflict for a person and is never overwritten. and hold 50 characters; a longer number is refused rather than cut. The dependency is soft: the event is subscribed by name, the Fakturownia package is never imported, and a simulated document never reaches a real account. BaseLinker's own is not used, because it would issue a second invoice in BaseLinker's numbering.
Demo mode
Without an account, runs every feature against a simulated BaseLinker account built from the store's own catalog, through the same parsers, planners and workflows as live mode. Rows are flagged , and the admin says it is a simulation on every view.
The demo writes only its own rows. The store's orders never get a simulated BaseLinker number, status or parcel in their metadata, and no event goes out for them; a demo plan or snapshot never replaces what a real account read. The snapshot is built by the job (every minute, demo mode only) or by Prepare now on the page (), never by opening a page. A warning goes to the log when demo mode runs in a production build.
Catalog: one card per variant, grouped under a main card for products with several variants, plus cards only BaseLinker has (one of them a bundle, one a main card with two variants), a card duplicated under the same SKU, an EAN on two cards, a renamed card and a few changed prices, so every plan has something to show.
Account: two price groups (one derived), a warehouse and a shop warehouse that cannot take stock, Allegro and Amazon order sources, a custom order field.
Directions: a visitor may switch the catalog and stock directions in the admin to try each plan; writers "succeed" against the simulation and the next simulated read reflects what they wrote.
Orders: store orders get BaseLinker ids in seconds and are "shipped" with an InPost number a few minutes later, in the plugin's rows. Deterministic Allegro and Amazon orders appear over the day (at most 5 a day, buyers at only); with , once a visitor arms the order import writer they become Medusa orders of the demo store (), and their statuses move on, one of them cancelled.
Returns and invoice numbers are simulated too. Nothing leaves Medusa.
Admin API
: configuration summary (never the token), counters, directions, writers with who armed them and when, references, journal state, the last run of each kind and, in demo mode, whether the snapshot exists. Reads the database only. Every route only reads, in demo mode too.
: what runs right now in any process (the job leases); the page polls it while a run is under way.
: demo mode only, builds the simulated snapshot now (idempotent).
: what the account offers, read now.
with : run a job now (202, background), under the same lease as the scheduled job.
with : arm or disarm a writer. Refused while the options switch it off.
with : demo mode only, to try the other directions.
: the plan of one direction.
: release a quarantined item.
: the card snapshot.
: cards of one product, for the product widget.
: the current plan of BaseLinker stock into Medusa.
: the outbox and the way back.
: send (or send again) one order now.
: the BaseLinker side of one order, for the order widget.
: marketplace orders.
: import one marketplace order now (needs the armed writer).
: returns.
: invoice numbers.
: write one invoice number again.
: the history.
, and : the koda.integration/1 contract, described in "Works with Koda Plus hosts".
No route deletes anything. Every write route takes a JSON body or the header (415 otherwise), and an unexpected error answers a plain sentence while the details stay in the server log.
Use it from your code
Also exported: (one pass of the outbox) and . reads the orders the plugin sent; the scheduled status job also follows the imported orders and the order journal, which does in one call.
Events on the Medusa event bus:
:
:
: , for sent orders () and for imported ones ()
:
:
: , kind , , or
:
The plugin listens to and, when the Fakturownia plugin is installed, to .
Order metadata the plugin writes in live mode, readable by your storefront through the Store API: , , , , , ; on imported orders also , , , and (and on orders the demo created). The plugin itself never decides anything by these keys: its tables are the record. Products created by the catalog import carry (and unless is ).
Security and personal data
Write barrier by method name, as described above: nothing outside the permitted methods leaves the process, and a writer that is not armed cannot use its method.
One HTTP client: the BaseLinker URL appears in one file, behind the barrier, a process-wide rate limiter and a timeout.
Masked token: the token and every token-like run of characters are masked in logs, stored errors and the admin, and so are e-mail addresses and international phone numbers an error may echo. The token never leaves the server: the admin only learns whether it is set.
Personal data: the plugin's own tables hold ids, statuses, totals and tracking, never buyer data. The order payload sent to BaseLinker is built at send time and not stored. An imported marketplace order carries the buyer's name, address, e-mail and phone on the Medusa order itself, as any order does, because the store has to ship it. Returns are stored without the buyer's data.
Reads only while rendering: the admin never calls BaseLinker and never writes to draw a page; network calls and the demo snapshot sit behind jobs and clicks.
One worker per item: leases in the plugin's own table (per job, per store order, per marketplace order, per invoice document) that hold across processes, plus the Medusa Locking module for the shared key, plus the lookups before every create.
Write guard: writes to need a JSON body or the header, so a form on another site cannot arm a writer or send an order with an admin's session cookie.
Metadata a buyer can write: the Store API takes any cart metadata and Medusa copies it to the order. The plugin decides by its own tables, never by order metadata (an order counts as imported only by its import row; a marketplace reference counts only on an order created without a cart), and the store routes refuse keys, and the skip key with 400 . A tax id or a note the buyer types stays allowed.
Works with Koda Plus hosts
An app that shows every Koda Plus plugin in one place (like medusa.koda.plus) reads BaseLinker through the shared contract and never needs to know its tables:
: the manifest (mode, configuration and what is missing, writers armed, the last run, widgets, the hosts an external link may point to: the carriers' tracking pages and baselinker.com).
(or , up to 50; also and ): one line per record, the worst row speaking. An order: in BaseLinker or imported is green, on its way is blue, a send retrying after an error, a failed Medusa fulfillment or a cancellation blocked by a fulfillment is orange, a failed send or import is red, and a skipped order is grey with its reason in the title key (, , , ). Its facts: for an imported order ("BaseLinker: Allegro", code , the external order reference, priority 60), as BaseLinker reports it (code , or , priority 50), with the carrier, the parcel number and its tracking page (priority 50), with the invoice number BaseLinker holds (priority 50, so the invoicing plugin's own document wins). A product: linked cards green, a card conflict on its SKU orange, an item held in quarantine red, and a fact while a stock plan has changes for it. An inventory item: in step with BaseLinker green, a difference waiting for the stock writer orange, a quarantined or failed stock write red, with a fact. Facts come from the plugin's rows (and from Medusa records read by id), never from order metadata.
: the counters and (red, orders), (red, inventory) and (orange, products), each with a link to its filtered list (, , , ).
The order card registers as for the zone (hidden while the order summary says "none") and the product card as for , both at tab order 60. A host that claims a zone shows the card as a tab (: no frame or header of its own, a line while loading, on an error and when there is nothing to show) and Medusa's own spot stays empty. Without a host nothing changes.
Public API
What other code may import: (the workflows, , , , and the rest listed in "Use it from your code"), (the module, its options and service) and . Type declarations ship with the package. Every other path is internal and may change in any release.
Running more than one process
Medusa can run a store as a server and a worker, or as several instances. The plugin keeps one run of each job and one worker per store order, marketplace order and invoice document across all of them with leases in its own table (, keys and ): a lease is renewed every minute while its holder works and expires 5 minutes after a process died. A click on Sync in the admin takes the same lease as the scheduled job, so it never runs next to it. The shared key also goes through the Medusa Locking module, which spans processes only with a Redis or Postgres provider. A lock store that does not answer is an error in the history and the log, never a quiet "busy". The rate limit () is counted per process: with a server and a worker, keep their sum under BaseLinker's 100 requests per minute.
Out of scope
It never deletes anything, in BaseLinker or in Medusa. A product removed in BaseLinker can only become a draft, and only with .
Cards created from Medusa are simple cards, one per variant: it does not build main cards with variants in BaseLinker. Card updates cover the name and the EAN only; descriptions, images, categories and manufacturers of existing cards are left as they are. BaseLinker does not document whether an update keeps the text fields that are not sent; the plugin relies on the per key behaviour the documentation describes for images, and the guide advises trying the cards writer on one card first.
The catalog import gives products one option axis () whose values are the variant names; it does not turn BaseLinker features into separate options like Colour and Size. Categories are matched by name, flat; the category tree is not rebuilt.
One catalog, one warehouse and one price group per store. Kits and bundles are left out of stock, prices and the import.
Orders that already reached BaseLinker stay as BaseLinker has them: edits, refunds and cancellations made in Medusa afterwards are not carried over, so change such an order in BaseLinker by hand. An order cancelled in Medusa before its send never goes out (a row with the code ).
Imported orders are fulfilled from BaseLinker statuses only when is set. An imported order that BaseLinker cancels after it was fulfilled in Medusa is flagged for a person, never cancelled automatically.
Returns are read only: no Medusa return or refund is created from them.
It does not send e-mails, and it cannot see your e-mail code: skipping the confirmation e-mail for marketplace orders is up to your subscriber.
The Allegro reference of imported orders assumes BaseLinker's of an Allegro order is the Allegro checkout form id, as BaseLinker documents it ("Allegro transaction number"); it has not been checked against an account connected to Allegro yet.
Uninstall
Disarm the writers in Settings (or switch them off with ) and set , so nothing is written while you remove the plugin.
Remove the plugin from and the package from .
What stays, on purpose: the plugin's tables (, , , , , , , , , ) with their history, the and keys in order metadata, the cards created in BaseLinker and the orders imported into Medusa. Drop the tables by hand when you no longer need the history; nothing in BaseLinker is deleted.
Compatibility
Medusa 2.12 to 2.21 (peer range ) and Node.js 20+. The release of each version runs the smoke test on a fresh Medusa 2.12.6 and 2.21.2 app: migrations, build, start and the admin pages. Developing the plugin needs Node.js 22.6+ (the tests run the TypeScript sources directly).
Development
covers the write barrier, token masking, response errors, the client retry policy, linking with variants and containers, every planner (catalog import, cards, stock both ways, prices) with the caps and the quarantine, the writers and their switches, the order payload and the totals computed from the lines, the marker scan, the leases across processes, the marketplace order import across a crash and a race, metadata a shopper could set, statuses and the journal, returns without personal data, invoice numbers, demo isolation, the admin reads that never write, the guards, the koda.integration/1 contract and backoff, without a network or a build.
Commercial support
Built and maintained by Koda Plus, a Medusa agency from Poland. The same BaseLinker logic runs in production since August 2026 at a Polish tyre and wheel retailer, next to our Allegro, OLX, Fakturownia and Subiekt nexo integrations. Need a custom integration or help with a migration to BaseLinker? Write to kontakt@koda.plus.
Trademarks
BaseLinker and Base are trademarks of their owner, used here only to identify the service this plugin connects to. This is an independent integration built on the public BaseLinker API, not affiliated with or endorsed by BaseLinker.
The full notes of every version are in CHANGELOG.md.
0.3.0 (unreleased)
Demo mode only with , and it writes only its own rows: the store's orders never get simulated numbers, statuses or parcels, and simulated marketplace orders become Medusa orders only with .
Reads never write; the demo snapshot comes from the job or Prepare now.
One run of each job and one worker per order across every process (leases), orders whose totals Medusa cannot compute still go, order metadata a shopper can set decides nothing.
The koda.integration/1 contract (summaries of orders, products and inventory items, board counters), embeddable order and product cards, deep links, type declarations.
0.2.1 (2026-10-07)
References: stores that start soon on Medusa can be listed with (a Soon badge, no link, optional); the since date is no longer shown, and an old in the options is ignored.
0.2.0 (2026-10-06)
Both catalog directions (): the catalog import from BaseLinker (prices of a price group, SKU, EAN, weight, images, description, categories by name, manufacturer; idempotent by the BaseLinker id; duplicates are conflicts; never a delete) and the card plan from Medusa (create and update, a lookup by SKU before every create, the new card linked at once).
Stock in both directions (): Medusa stock into a warehouse with , plan first, capped, per item quarantine.
Price push into a BaseLinker price group with , gross prices from net Medusa prices with the card's VAT rate.
Seven writers, each off by default, with a hard switch in the options () and an arm switch in the admin that records who and when.
Marketplace orders into Medusa, exactly once per BaseLinker order: chosen sources, region and sales channel, lines by card link, prices from the order, delivery as a shipping method, taxes from the region, payment, inventory reserved, shared with the other Koda Plus plugins and checked under a shared lock, loop guard in both directions, cancellation while nothing is fulfilled.
Returns, read only, without buyer data, linked to Medusa orders.
Faster status pickup through the BaseLinker order journal, with the full read as a fallback.
Invoice numbers from the Fakturownia plugin into a BaseLinker order field, exactly once, never over another value.
Variant cards read with ; main cards with variants are containers, never linked.
Admin: Panel and Setup guide views (), the directions and writers matrix, every plan with filters and quarantine release, marketplace orders, returns, invoice numbers, the account lists in the connection check, references ("Running in production"), widgets for imported orders and main cards.
Demo mode simulates all of it: duplicated SKUs and EANs, price groups, both stock directions, deterministic Allegro and Amazon orders (at most 5 a day, buyers) that become real Medusa orders, returns and invoice numbers.
Six new tables (, , , , , ): run .
0.1.0 (2026-10-05)
First public release: card linking by SKU and EAN with conflicts never linked, stock planned before it is written (BaseLinker to Medusa), store orders exactly once with a marker scan, statuses, tracking and fulfillment back, admin page and widgets in English and Polish, demo mode.