Allegro Marketplace
Allegro offers and SKU sync
@zanreal/medusa-allegro
Medusa v2 plugin for Allegro, the largest marketplace in Poland.
Full documentation, in English and Polish, is published at https://zanreal.com/docs/oss/medusa-allegro and authored in Copy to clipboarddocs/.
Status: pre-release. The full sync engine is here: offer discovery, a read-only pricing monitor, price-automation writes, the quantity push, and the order event drain. Treat the schema as settled and the API surface as still moving until 1.0.
What is here:
- A zero-dependency, fetch-based Allegro REST client, ported from a production integration. Offers, promo options, price-automation rules and commands, order events, checkout forms, categories, fee preview.
- OAuth 2.0 authorization-code flow with a CSRF-protected callback, refresh-token rotation, and AES-256-GCM encryption of both tokens at rest.
- Offer discovery matches every seller offer's sygnatura to a Medusa variant SKU, sweeps promotion state in one paginated pass, creates category rate rows, and records conflicts instead of guessing.
- A read-only pricing monitor records each offer's price mode, attached rule and drift, and audits real rule transitions.
- Price sync attaches the rule the promotion state calls for and asserts Copy to clipboard
[break-even, SRP]bounds, with a per-run change cap, per-offer quarantine, a circuit breaker, and write-scope detection. - Stock push reconciles Medusa's available quantity into Allegro through the quantity-change command.
- Order sync drains Copy to clipboard
GET /order/eventsinto Medusa orders, with fulfillment write-back and an operator import window for gaps beyond the event retention period. - Invoice attach puts an issued invoice PDF onto the Allegro order, driven by an event from an invoicing module and retried by a bounded sweep. A soft dependency in one direction only - see The invoice chain.
- Admin surfaces built around one rule - per-product state on the product, everything else under Settings. A product detail widget shows each variant SKU's linked offer, status, drift, promotion state and per-offer price sync opt-out (with a push-history drawer); a compact product-list banner rolls up linked / drifting / conflicting counts; the connection, writer toggles, category rates, the cross-catalogue offers table and the orders task-flow all live under Settings -> Allegro, with nothing Allegro-specific in the main ecommerce sidebar. See Admin UI.
Nothing writes to Allegro until you arm it. Every writer is governed by a persisted, admin-flippable toggle that ships off on a fresh install, so a newly connected store publishes nothing until an operator arms each writer under Settings -> Allegro - no redeploy needed to arm or disarm, and the environment can still hard force-disable any writer regardless. On top of that: price sync is inert without Copy to clipboardautomationRules, and a fresh install starts its order cursor at "now" rather than importing history. See Runtime toggles and Turning the writers on.
Install
Copy to clipboard@zanreal/medusa-allegro is on npm:
1npm install @zanreal/medusa-allegro
That resolves to a prebuilt tarball - the published package already contains the Copy to clipboard.medusa/server output its Copy to clipboardexports point at, so nothing needs to compile on install. It pulls Copy to clipboard@zanreal/medusa-admin-kit from the registry too, for the Catalog columns it contributes.
Copy to clipboardmain keeps moving after a release ships (see Releasing), so if you need a fix or feature that has landed on Copy to clipboardmain but not yet been released, install it as a git dependency instead, pinned to a commit:
123456// package.json{"dependencies": {"@zanreal/medusa-allegro": "github:zanreal-labs/medusa-allegro#b0e864ab6a05e63e6d57a20b3c8adaf943049cdd"}}
Pin the commit you tested against. Copy to clipboard#main would move under you on the next push.
Installed that way, the package compiles itself on install - Copy to clipboardprepare runs Copy to clipboardmedusa plugin:build, which turns the checked-out source into the Copy to clipboard.medusa/server output its Copy to clipboardexports point at. pnpm 10 and newer refuse to run that script for a dependency they do not already trust, so a fresh install needs it allowed once in your own project:
123# pnpm-workspace.yamlallowBuilds:"@zanreal/medusa-allegro@https://codeload.github.com/zanreal-labs/medusa-allegro/tar.gz/b0e864ab6a05e63e6d57a20b3c8adaf943049cdd": true
The key is the exact tarball URL pnpm resolves the pinned commit to, which is why it carries the same SHA as the dependency line - move both together.
Register it in Copy to clipboardmedusa-config.ts:
1234567891011121314151617181920import { defineConfig } from "@medusajs/framework/utils";module.exports = defineConfig({plugins: [{resolve: "@zanreal/medusa-allegro",options: {clientId: process.env.ALLEGRO_CLIENT_ID,clientSecret: process.env.ALLEGRO_CLIENT_SECRET,environment: process.env.ALLEGRO_ENVIRONMENT ?? "production",// App identity. `appName` must match the app registered in the Allegro// Developer Portal; Allegro rejects requests whose User-Agent does not// identify a real app.appName: "MyStoreAllegro",appVersion: "1.0.0",docsUrl: "https://mystore.example.com/integrations/allegro",// openssl rand -base64 32encryptionKey: process.env.ALLEGRO_ENCRYPTION_KEY,
Then run the migrations:
1npx medusa db:migrate
Options
Option Type Required Default Notes Copy to clipboardclientId Copy to clipboardstring yes - Allegro application client id. Copy to clipboardclientSecret Copy to clipboardstring yes - Allegro application client secret. Copy to clipboardenvironment Copy to clipboard"production" | "sandbox" no Copy to clipboard"production" Sandbox talks to Copy to clipboardapi.allegro.pl.allegrosandbox.pl. Copy to clipboardappName Copy to clipboardstring yes - Must match the registered app name. No whitespace or HTTP separators. Copy to clipboardappVersion Copy to clipboardstring yes - Your integration version, e.g. Copy to clipboard"1.0.0". Copy to clipboarddocsUrl Copy to clipboardstring yes - Public http(s) URL documenting or contacting the integration. Copy to clipboardencryptionKey Copy to clipboardstring yes - Base64-encoded 32 bytes. Seals the stored tokens. Rotating it makes existing tokens unreadable: reconnect after a rotation. Copy to clipboardredirectPath Copy to clipboardstring no Copy to clipboard"/admin/allegro/oauth/callback" A rooted path on this backend, matching the redirect URI registered for the app character for character. Copy to clipboard//host/... is rejected: it is a protocol-relative URL, not a path. Copy to clipboardscopes Copy to clipboardstring no Copy to clipboard"allegro:api:sale:offers:read allegro:api:sale:offers:write allegro:api:orders:read" Space-separated. Drop Copy to clipboard:write if you only ever want read access; the plugin then reports the missing write scope in the UI. Copy to clipboardpriceSyncDisabled Copy to clipboardboolean no Copy to clipboardfalse Force-disable override for price writes. It can only force the writer OFF - the live arming is the persisted runtime toggle. Must be a real boolean: a string throws at boot rather than failing open. Prefer Copy to clipboardALLEGRO_PRICE_SYNC_DISABLED for the env-driven incident case. Copy to clipboardfulfillmentWritebackDisabled Copy to clipboardboolean no Copy to clipboardfalse Force-disable override for the fulfillment write-back (the seller-status push on a Medusa fulfillment/shipment). NEW: this event-driven writer had no kill switch before. Live arming is its runtime toggle; env is Copy to clipboardALLEGRO_FULFILLMENT_WRITEBACK_DISABLED. Copy to clipboardinvoiceAttachDisabled Copy to clipboardboolean no Copy to clipboardfalse Force-disable override for attaching invoice PDFs to Allegro orders. Its own switch, not a reading of Copy to clipboardordersSyncDisabled - see The invoice chain. Same boolean-only contract. Copy to clipboardbackendUrl Copy to clipboardstring no derived Absolute base URL of this backend. Set it when a proxy rewrites Copy to clipboardHost. Falls back to Copy to clipboardMEDUSA_BACKEND_URL, then the request.
Sync options
Option Type Required Default Notes Copy to clipboardpricingMode Copy to clipboard"monitor" | "automation_rule" | "fixed_price" no Copy to clipboard"automation_rule" How this store prices its Allegro offers - see Pricing modes. This is the DEFAULT; the persisted admin choice wins over it. Copy to clipboard"automation_rule" is what this plugin did before the mode existed, so an upgrade changes nothing. Copy to clipboardautomationRules Copy to clipboard{ promoted: string; standard: string } no - Names of two price-automation rules that must already exist on the Allegro account. Resolved by name every run; missing, renamed or ambiguous aborts the run with nothing written. Omit it and price sync is inert. Also editable and persisted from the admin - see Sync configuration fields. Copy to clipboardchangeCap Copy to clipboardnumber no Copy to clipboard1 Price-automation commands per run. Positive integer; Copy to clipboard0 is rejected - use a kill switch to stop writes, not a zero cap. Also editable and persisted from the admin. The default is a deliberately minimal placeholder, not a recommendation - see Choosing a change cap. Copy to clipboardstockSyncDisabled Copy to clipboardboolean no Copy to clipboardfalse Force-disable override for quantity writes. Can only force OFF; the live arming is the runtime toggle. Same boolean-only contract as Copy to clipboardpriceSyncDisabled. Copy to clipboardordersSyncDisabled Copy to clipboardboolean no Copy to clipboardfalse Force-disable override for the order drain. Forced off, the journal is not consumed at all, so the cursor holds and nothing is skipped. Live arming is the runtime toggle. Copy to clipboardsalesChannelId Copy to clipboardstring no - Scopes which products are sync-eligible. With neither this nor Copy to clipboardsalesChannelName, the whole catalogue is eligible. Also editable and persisted from the admin - wiring-critical, see Sync configuration fields. Copy to clipboardsalesChannelName Copy to clipboardstring no - Resolved by name at run time. A configured name that does not exist is an error, not a fallback to the whole catalogue. Also editable and persisted from the admin. Copy to clipboardstockLocationIds Copy to clipboardstring[] no every location Locations whose available quantity is summed for the push. Copy to clipboardALLEGRO_STOCK_LOCATION_IDS overrides it. Copy to clipboardsrpMetadataKey Copy to clipboardstring no - Reads the SRP (the price-range ceiling) from that key in the variant's Copy to clipboardmetadata, falling back to the product's. Mutually exclusive with Copy to clipboardsrpPriceListId. Also editable and persisted from the admin. Copy to clipboardsrpPriceListId Copy to clipboardstring no - Reads the SRP from the variant's price in that price list. Also editable and persisted from the admin. Copy to clipboardcostsModuleKey Copy to clipboardstring no Copy to clipboard"productCosts" Container key of Copy to clipboard@zanreal/medusa-product-costs, resolved lazily and optionally. Without it, every offer is skipped with Copy to clipboardmissing-break-even. There is never a default floor. Copy to clipboardinvoiceModuleKey Copy to clipboardstring no Copy to clipboard"infakt" Container key of the invoicing module that issues your documents, resolved lazily and optionally. Without it the invoice chain is inert. See The invoice chain. Copy to clipboardmarketplaceId Copy to clipboardstring no Copy to clipboard"allegro-pl" Marketplace the rule assignment targets. Also editable and persisted from the admin - wiring-critical, see Sync configuration fields. Copy to clipboardregionId Copy to clipboardstring no derived Region Allegro orders are created in. Falls back to the first region matching the order currency, then the first region at all (with a warning).
All options are validated in a module loader, so a misconfiguration fails at boot with a specific message instead of surfacing as an opaque Allegro error later. The validations worth knowing about, because each catches a mistake that would otherwise present as a silently inert loop:
- A boolean-looking string on any kill switch throws. Copy to clipboard
priceSyncDisabled: process.env.Xyields Copy to clipboard"true", which a truthiness test honours and a Copy to clipboard=== truetest ignores - the switch would read as enabled while you believed it was off. - One rule name used for both promotion states throws. A promotion flip would then be a no-op switch, so the promoted commission rate would never reach the price floor, and price sync would look healthy while systematically under-flooring every promoted offer.
- Both SRP sources set at once throws. The ceiling is what stops an automation rule ratcheting a price down; two sources means an ambiguous ceiling.
Environment variables
Variable Effect Copy to clipboardALLEGRO_PRICE_SYNC_DISABLED Copy to clipboard1, Copy to clipboardtrue or Copy to clipboardyes force-disables price writes, beating a persisted toggle that is armed. A hard override that can only force OFF - it never arms a writer. The env wins on purpose: an operator setting it is responding to an incident. Copy to clipboardALLEGRO_STOCK_SYNC_DISABLED The same, for quantity writes. Copy to clipboardALLEGRO_PRICE_SYNC_DISABLED alone does not stop all writes - the quantity command is a separate writer. Copy to clipboardALLEGRO_ORDERS_SYNC_DISABLED The same, for the order drain. Copy to clipboardALLEGRO_FULFILLMENT_WRITEBACK_DISABLED The same, for the fulfillment write-back (the seller-status push on a Medusa fulfillment/shipment). NEW - this event-driven writer previously had no kill switch. See Fulfillment write-back. Copy to clipboardALLEGRO_INVOICE_ATTACH_DISABLED The same, for attaching invoice PDFs. A separate switch from the drain on purpose - see The invoice chain. Copy to clipboardALLEGRO_OFFER_SYNC_CRON Schedule for the hourly catalogue pass (discovery, monitor, price sync). Default Copy to clipboard"15 * * * *". Copy to clipboardALLEGRO_STOCK_SYNC_CRON Schedule for the quantity backstop. Default Copy to clipboard"*/15 * * * *". Quantities normally reach Allegro through the event path (a sale, or Copy to clipboardmarken.stock.changed) within seconds; this sweep repairs what that missed - see Copy to clipboardjobs/allegro-stock-sync.ts for the exact list. Copy to clipboardALLEGRO_ORDERS_SYNC_INTERVAL_MS Interval, in ms, for the order drain. Default Copy to clipboard20000 (20s). The drain schedules on an interval by default because Medusa's cron only resolves to the minute and a fresh order should be drained sub-minute. Copy to clipboardALLEGRO_ORDERS_SYNC_CRON Switches the order drain back to a cron expression instead of an interval. The two are mutually exclusive in Medusa's scheduler; when both are set the cron wins. Copy to clipboardALLEGRO_ORDERS_RECONCILE_UNPAID_INTERVAL_MS Minimum gap, in ms, between reconciliation sweeps of orders with no registered payment. Default Copy to clipboard0 - every drain tick, i.e. every 20s. An unpaid order is one the buyer may be paying right now, so this is deliberately the fastest thing the plugin does. Copy to clipboardALLEGRO_ORDERS_RECONCILE_OPEN_INTERVAL_MS The same, for orders that are paid but not finished (awaiting shipment, in transit). Default Copy to clipboard900000 (15 min): a lost event here delays a status label, not the money. Copy to clipboardALLEGRO_ORDERS_RECONCILE_BATCH Maximum orders re-read from Allegro per sweep. Default Copy to clipboard50. Allegro's global limit is 9000 requests/minute per client id and the order endpoints carry no per-resource limit, so this is a backlog guard rather than a rate-limit one. Copy to clipboardALLEGRO_ORDERS_RECONCILE_SENT_GRACE_MS How long a shipped Medusa fulfillment is left to the Copy to clipboardshipment.created subscriber before the sweep pushes Copy to clipboardSENT itself. Default Copy to clipboard600000 (10 min). It exists because Allegro's checkout-form read model lags its own writes by about 45 seconds (measured 2026-08-25: a 2xx Copy to clipboardPUT .../fulfillment followed by a Copy to clipboardGET that still read Copy to clipboardREADY_FOR_SHIPMENT, correct only on a re-read ~45s later), so a sweep straight after a successful push would still see Copy to clipboardREADY_FOR_SHIPMENT and re-push. The same lag means a status read back immediately after a push is stale rather than evidence the push failed. Set it to Copy to clipboard0 only if you have disabled the subscriber. Copy to clipboardALLEGRO_STOCK_LOCATION_IDS Comma-separated stock location ids, overriding Copy to clipboardstockLocationIds. Copy to clipboardALLEGRO_PRICING_MODE Locks the pricing mode, beating both the admin picker and Copy to clipboardpricingMode. Ignored (read as unset) unless it names a real mode. See Pricing modes. Copy to clipboardALLEGRO_AUTOMATION_RULE_STANDARD Locks the standard-offer automation rule name, beating both the admin field and Copy to clipboardautomationRules.standard. See Sync configuration fields. Copy to clipboardALLEGRO_AUTOMATION_RULE_PROMOTED The same, for the promoted-offer rule name. Copy to clipboardALLEGRO_SRP_METADATA_KEY The same, for the SRP metadata key. Copy to clipboardALLEGRO_SRP_PRICE_LIST_ID The same, for the SRP price list id. Copy to clipboardALLEGRO_CHANGE_CAP The same, for the per-run change cap. Ignored (read as unset) unless it is a positive integer. Copy to clipboardALLEGRO_MARKETPLACE_ID The same, for the marketplace id. Wiring-critical - see Sync configuration fields. Copy to clipboardALLEGRO_SALES_CHANNEL_ID The same, for the sales-channel id. Wiring-critical. Copy to clipboardALLEGRO_SALES_CHANNEL_NAME The same, for the sales-channel name. Copy to clipboardMEDUSA_BACKEND_URL Fallback for Copy to clipboardbackendUrl when deriving the OAuth redirect URI.
The schedules and force-disable overrides are env vars rather than plugin options because Medusa evaluates a scheduled job's Copy to clipboardschedule at plugin-load time, before the DI container - and therefore this plugin's Copy to clipboardoptions - exists. There is no way to read a module's resolved options from that static export.
The schedules start firing as soon as the plugin loads, but the writers are armed by the persisted toggles, which ship off. Installing or upgrading this plugin runs the loops on their cadence, but every writer stays disarmed until you arm it under Settings -> Allegro (or the environment force-disable, if set, keeps it off regardless). The read paths are harmless (discovery and the monitor write nothing to Allegro). If you are staging a cutover from another system and want belt-and-braces, set the force-disable env vars BEFORE the version that reads them ships. See Runtime toggles and Turning the writers on.
Pricing modes
How this store prices its Allegro offers is a setting, not an assumption. Pick one of three modes under Settings -> Allegro; it takes effect on the next sync run, with nothing to restart.
Mode What it writes to Allegro Copy to clipboardmonitor (Monitor only) Nothing at all. Every run still works out each linked offer's break-even floor and SRP ceiling and counts how many offers sit outside them. Copy to clipboardautomation_rule (Allegro automation rule) One Copy to clipboardPOST /sale/offer-price-automation-commands per offer: attach the named rule its promotion state calls for, with Copy to clipboard[floor, ceiling] as the rule's price range. Allegro's engine then picks the number inside that range. Copy to clipboardfixed_price (Fixed price from Medusa) One Copy to clipboardPUT /sale/offer-price-change-commands/{id} per offer, setting the Buy Now price to the variant's own Medusa price - preceded by a rule-REMOVAL command when the offer still carries an automation rule.
Copy to clipboardautomation_rule is the default, because it is what this plugin did before the mode existed. Upgrading changes nothing about what your store writes.
The floor and the ceiling apply in every mode
The break-even floor (from Copy to clipboard@zanreal/medusa-product-costs, grossed for VAT and for the category commission) and the SRP ceiling (from variant metadata or a price list) are the safety story of this whole plugin, so no mode is allowed to skip them:
- Copy to clipboard
monitorcomputes both and reports how many offers are priced outside them. That report is what you read before choosing a mode that writes. - Copy to clipboard
automation_rulesends them as the rule's price range, so Allegro's engine cannot move the price outside them. - Copy to clipboard
fixed_pricechecks the Medusa price against them and refuses to push a price below the floor or above the ceiling, counting it as Copy to clipboardprice-outside-bounds.
A refusal is deliberate, and it is not a clamp. Clamping would sell at a price the store never set; pushing would sell below cost. Refusing does neither, and it names the variants whose Medusa price needs fixing.
What fixed-price mode needs, and what it does about it
Two things are true and worth stating plainly before you switch a live store to it:
- The scope is already there. Copy to clipboard
PUT /sale/offer-price-change-commands/{commandId}needs Copy to clipboardallegro:api:sale:offers:write, which is in this plugin's default scope string and is the same scope the rule assignment already uses. Moving to fixed-price mode needs no reconnect and no new consent. - An automation rule beats a fixed price, so the rule has to go first. Allegro's engine recalculates an offer on its own schedule, so a price pushed under a live rule does not survive it. Fixed-price mode therefore issues Copy to clipboard
POST /sale/offer-price-automation-commandswith a Copy to clipboardremovemodification for the offer's marketplace, waits for it to confirm, and only then sets the price. If the removal does not confirm, the price is not sent at all - a half-applied pair that left the rule attached and the price changed is precisely the fight with Allegro's engine the sequencing exists to avoid. Re-running the pair next tick is idempotent.
Both commands count as one offer against the per-run change cap.
Where the fixed price comes from
The variant's own default price in Medusa, in the offer's own currency. Two rules, both fail-closed:
- Price-list rows are ignored. A price carrying a Copy to clipboard
price_list_idis a sale or a customer-group override with its own validity window and conditions, none of which this plugin evaluates. Pushing one would leave a sale price on Allegro long after the sale ended. - There is no currency conversion. A variant with no price in the offer's currency is skipped as Copy to clipboard
missing-medusa-pricerather than priced from a rate this plugin cannot audit.
Auditing
Every mode writes to the same append-only Copy to clipboardallegro_price_push trail, but they fill different columns, and the difference is load-bearing:
- automation-rule rows carry Copy to clipboard
bound_floor/ Copy to clipboardbound_ceilingand the rule ids. Those two columns are the only memory of the price range attached to a rule, because Allegro accepts a range and never returns one. - fixed-price rows carry Copy to clipboard
price_amount/ Copy to clipboardprice_currencyand leave the bounds columns null, plus Copy to clipboardrule_id_old/ Copy to clipboardrule_name_oldfor the rule that was removed. Writing the guard rails into the bounds columns would make a later automation-rule run read back a price range that was never attached, and skip an offer it should have re-attached.
Monitor mode and the price-write toggle
Monitor mode runs even while the Price writes toggle is off, because it has no command path to reach - exactly like the read-only price-automation monitor, which has never had a kill switch. The two writing modes honour the toggle as they always have, re-reading it before every single command. An explicit per-offer push from a product page is refused in monitor mode rather than quietly performed.
Runtime toggles
Every writer that reaches Allegro is governed by a persisted, operator-flippable toggle, stored as a one-row Copy to clipboardallegro_settings singleton. This is the live arming an operator controls from Settings -> Allegro - flip a switch and it takes effect on the next tick or event, with no redeploy, because every runtime path resolves its effective state from the persisted row at the top of each run rather than from a value captured at boot.
The five governed writers:
Toggle Column Fresh-install default Env force-disable Price writes Copy to clipboardprice_sync_enabled off Copy to clipboardALLEGRO_PRICE_SYNC_DISABLED Quantity writes Copy to clipboardstock_sync_enabled off Copy to clipboardALLEGRO_STOCK_SYNC_DISABLED Order drain Copy to clipboardorders_sync_enabled off Copy to clipboardALLEGRO_ORDERS_SYNC_DISABLED Fulfillment write-back Copy to clipboardfulfillment_writeback_enabled off Copy to clipboardALLEGRO_FULFILLMENT_WRITEBACK_DISABLED Invoice attach Copy to clipboardinvoice_attach_enabled on (inert) Copy to clipboardALLEGRO_INVOICE_ATTACH_DISABLED
Precedence
The environment (and the boot-time plugin option) is a hard override that can only force a writer OFF. It never arms one. The effective state is:
1effectiveEnabled = persistedEnabled && !forceDisabled
So:
- persisted on + env unset -> on (the writer runs)
- persisted on + env force-disable -> off (the override wins; an operator responding to an incident is not undone by a stale armed toggle)
- persisted off + env unset -> off (nothing arms a writer but the toggle)
The admin shows a switch that the environment forces off as locked and "forced off by environment", with the env var to clear - it never renders an armed-looking switch for a writer the environment is holding down. A write to a forced-off toggle is still accepted and stored, so the intent is preserved for when the override is lifted.
Fresh-install defaults
Every writer ships off, so a freshly connected store publishes nothing to Allegro until an operator arms each writer deliberately. Invoice attach is the one exception - it ships on but inert: by the time an invoice event reaches this plugin the document already exists as a legal record, so delivering it is the safe default, and there is nothing to attach until an invoicing module is wired and emitting events.
The singleton row is created lazily under a fixed primary key on first read, with these defaults. Upgrading an existing install runs the additive migration that creates the table; the row appears the first time any runtime path or the admin reads it.
Sync configuration fields
Nine settings - the pricing mode, the two automation rule names, the SRP source, the change cap, the marketplace id and the sales-channel scope - are editable from Settings -> Allegro, on the same Copy to clipboardallegro_settings singleton the runtime toggles use. An edit persists and takes effect on the next sync run, no redeploy - the same property the toggles have. A store that never touches these admin fields behaves exactly as before: every persisted column starts Copy to clipboardnull, and Copy to clipboardnull falls through to the Copy to clipboardmedusa-config.ts option.
Field Column Copy to clipboardmedusa-config.ts option Env lock Pricing mode Copy to clipboardpricing_mode Copy to clipboardpricingMode Copy to clipboardALLEGRO_PRICING_MODE Automation rule (standard) Copy to clipboardautomation_rule_standard Copy to clipboardautomationRules.standard Copy to clipboardALLEGRO_AUTOMATION_RULE_STANDARD Automation rule (promoted) Copy to clipboardautomation_rule_promoted Copy to clipboardautomationRules.promoted Copy to clipboardALLEGRO_AUTOMATION_RULE_PROMOTED SRP source: metadata key Copy to clipboardsrp_metadata_key Copy to clipboardsrpMetadataKey Copy to clipboardALLEGRO_SRP_METADATA_KEY SRP source: price list id Copy to clipboardsrp_price_list_id Copy to clipboardsrpPriceListId Copy to clipboardALLEGRO_SRP_PRICE_LIST_ID Change cap Copy to clipboardchange_cap Copy to clipboardchangeCap Copy to clipboardALLEGRO_CHANGE_CAP Marketplace id (wiring-critical) Copy to clipboardmarketplace_id Copy to clipboardmarketplaceId Copy to clipboardALLEGRO_MARKETPLACE_ID Sales channel id (wiring-critical) Copy to clipboardsales_channel_id Copy to clipboardsalesChannelId Copy to clipboardALLEGRO_SALES_CHANNEL_ID Sales channel name Copy to clipboardsales_channel_name Copy to clipboardsalesChannelName Copy to clipboardALLEGRO_SALES_CHANNEL_NAME
Precedence
Adapted from the toggles' "the override can only force off" contract to a value rather than a boolean - there is no "off" for a string or a number, so a set environment lock wins outright:
1effectiveValue = envLock ?? persistedValue ?? medusaConfigDefault
- an env lock, when set, is authoritative - it beats both a persisted admin edit AND the Copy to clipboard
medusa-config.tsoption, and the admin shows the field locked, the same treatment a forced-off toggle gets - otherwise the persisted admin value governs, when one has been entered
- otherwise the Copy to clipboard
medusa-config.tsoption governs, exactly as it always did
Clearing a field in the admin (blank it and Save) writes Copy to clipboardnull, which falls back to the Copy to clipboardmedusa-config.ts option rather than to an empty value - the same "clear" contract the category-rates page already uses.
Marketplace id and sales channel id are wiring-critical
Editing Copy to clipboardmarketplaceId or Copy to clipboardsalesChannelId re-scopes which Medusa products this plugin matches against Allegro offers, not merely a tuning knob - a wrong value breaks the mapping silently rather than producing an obviously bad result. Both stay editable and persisted, with the same env-lock escape hatch as everything else: set Copy to clipboardALLEGRO_MARKETPLACE_ID or Copy to clipboardALLEGRO_SALES_CHANNEL_ID to pin the correct value against an admin mistake during a cutover. The admin renders an explicit warning on both inputs saying so.
Automation rule names and SRP source stay mutually consistent
Two invariants that already existed as boot-time checks in Copy to clipboardresolveAllegroOptions - the standard and promoted rule names must differ, and at most one SRP source may be set - are now also enforced on every admin write, because persisting a field independently of the other can newly create a collision the boot-time check never saw (one half configured, the other newly persisted to the same value). The write is rejected with a Copy to clipboardMedusaError rather than silently accepted.
OAuth setup
1. Register an application with Allegro
- Sign in to apps.developer.allegro.pl with the seller account you want to connect (use apps.developer.allegro.pl.allegrosandbox.pl for sandbox).
- Create a new application. Choose the type that has a web application with a redirect URI - the plugin uses the authorization-code grant, not the device flow.
- Name it something stable and machine-safe, with no spaces: Copy to clipboard
MyStoreAllegro. This exact string goes into the Copy to clipboardappNameoption, because Allegro requires every request to carry a Copy to clipboardUser-Agentthat identifies the registered app one-to-one. The plugin composes Copy to clipboard{appName}/{appVersion} (+{docsUrl})and validates it at construction time, so a name with a space is rejected before any request is sent. - Set the redirect URI to your backend plus Copy to clipboard
redirectPath: Allegro compares this byte for byte during the token exchange. A trailing slash difference is a failed connection.
1https://your-medusa-backend.example.com/admin/allegro/oauth/callback
- Grant the app the scopes you configured: offer read, offer write, order read.
- Copy the client id and secret into Copy to clipboard
ALLEGRO_CLIENT_IDand Copy to clipboardALLEGRO_CLIENT_SECRET.
2. Generate an encryption key
1openssl rand -base64 32
Put it in Copy to clipboardALLEGRO_ENCRYPTION_KEY. The plugin refuses to boot unless the value is canonical base64 (standard or URL-safe) for exactly 32 bytes, rather than silently accepting a weak one. The check is deliberately strict about the encoding and not only the length, because Copy to clipboardBuffer.from(value, "base64") never throws: it drops every character outside the alphabet, so a length test alone accepts mangled input, and Copy to clipboard"A".repeat(43) decodes to a well-formed all-zero key. Both are rejected.
The key also signs the OAuth Copy to clipboardstate (see below), so rotating it invalidates any connection flow that is mid-air as well as the stored tokens.
3. Connect from the admin
Open Settings -> Allegro in the Medusa Admin and click Connect Allegro. You land on Allegro's consent screen, approve, and come back to the settings page with the account login, granted scopes, and token expiry filled in.
The page distinguishes three unhealthy states from a working connection, because each needs a different response: a missing refresh token (reconnect before the access token expires), an unreadable token envelope (the Copy to clipboardencryptionKey no longer opens what is stored - restore the old key or reconnect), and price sync disabled. A row whose envelope will not open is reported as such rather than as a green "Connected", which would send you looking at Allegro instead of at your own configuration.
How the flow is protected
- Copy to clipboard
GET /admin/allegro/oauth/startmints a Copy to clipboardstate, parks it in an httpOnly Copy to clipboardSameSite=Laxcookie with a 10-minute lifetime, and returns the authorization URL for the admin to navigate to. Over https the cookie carries the Copy to clipboard__Host-prefix, so a sibling subdomain cannot shadow it; over plain http it does not, because a Copy to clipboard__Host-cookie without Copy to clipboardSecureis dropped and local development would break. - The Copy to clipboard
stateis not an opaque nonce. It is Copy to clipboardv1.<issuedAt>.<nonce>.<mac>, where the MAC is HMAC-SHA256 over the mint time, the nonce and the admin user's Copy to clipboardactor_id, keyed by Copy to clipboardencryptionKey. The admin id itself is not in the value, because the value travels through Allegro's authorize URL and into browser history and access logs. - Copy to clipboard
GET /admin/allegro/oauth/callbackrequires that Copy to clipboardstateto match the cookie, compared in constant time, and to verify against the actor completing the flow. The cookie proves same-browser; the signature proves same-server, same admin, and minted within the last ten minutes. A state planted in someone else's browser fails the second check. - The state cookie is cleared only once the authorization code has actually been handed to Allegro - which is when the state is spent, so it stays single-use. Branches that run before the state is verified (Copy to clipboard
?error=..., a missing code, a state mismatch) deliberately leave it alone, so a lured GET to the callback cannot destroy a flow the operator legitimately started in another tab. - Both routes live under Copy to clipboard
/admin, which Medusa authenticates by default. The callback keeps that default: Allegro's redirect back is a top-level GET navigation and Medusa's admin session cookie is Copy to clipboardSameSite=Lax, so the session survives the hop. Making it public would also remove the Copy to clipboardactor_idthe signed state is verified against, so every flow would fail instead.
If your deployment authenticates the admin with a bearer token in local storage rather than a session cookie, the callback will 401, because the browser has no cookie to send on that navigation. Serve the admin and the backend on the same origin with session auth; do not make the callback public.
Disconnecting
Copy to clipboardPOST /admin/allegro/disconnect revokes the refresh and access tokens at Allegro and then deletes the stored row. Revocation is best-effort - if Allegro is unreachable the local connection is still removed, because refusing to disconnect would leave an operator unable to remove access they asked to remove.
When revocation is skipped or fails, the response carries a Copy to clipboardwarning and the settings page shows it. That matters here more than in most places: the stored rows are the only copy of the tokens, so after this call there is nothing left to revoke with, and the refresh token stays valid at Allegro until it expires unless you remove the application's access by hand in the developer panel.
Admin UI
The information architecture answers two things at once: you should not have to open a separate table to see a product's Allegro state, and nothing Allegro-specific belongs in the main ecommerce sidebar - it is an integration's configuration and operator tooling, not a merchandising surface, so every non-per-product view lives under Settings.
- Product detail widget (Copy to clipboard
product.details.after) - the authoritative per-product view. For every variant SKU it shows the linked offer (with a link to the live listing), a short status (linked / not linked / conflict), the observed price mode and drift, promotion state, the last price and stock sync times, and the per-offer price sync opt-out switch. The push history - the only record of the bounds ever sent - opens in a drawer. This is where an operator checks or opts a single product out, without touching the catalogue table. It fetches its own variants, and must. The dashboard loads the product for this zone with Copy to clipboardPRODUCT_DETAIL_FIELDS = getLinkedFields("product", "*categories,*shipping_profile,-variants")(Copy to clipboard@medusajs/dashboard/src/routes/products/product-detail/constants.ts). That Copy to clipboard-variantsis an explicit exclusion - the page fetches the variant table separately with Copy to clipboarduseProductVariants- so Copy to clipboarddata.variantshanded to a Copy to clipboardproduct.details.*widget is Copy to clipboardundefined. This widget derived its SKU list from Copy to clipboarddata.variantsand returned Copy to clipboardnullwhen that list was empty, which meant it rendered nothing, on every product, on every store. It now calls Copy to clipboardsdk.admin.product.listVariants(productId, { fields: "id,title,sku" })itself and still prefers Copy to clipboarddata.variantsif a future dashboard version passes it. - Product list banner (Copy to clipboard
product.list.before) - a compact roll-up (N linked / N unlinked / N drifting / N conflicts) above the stock products table, each count linking into Settings -> Allegro offers filtered to those rows. Medusa 2.18 does not allow injecting a custom column into the core products data table, and it exposes no list-row widget zone, so on that page a roll-up is the only thing a plugin can offer. It stays as the zero-dependency fallback for a store that has not installed admin-kit. - Catalog columns (Copy to clipboard
src/admin/widgets/register-variant-columns.tsx) - two columns registered into Copy to clipboard@zanreal/medusa-admin-kit's Catalog route, which lists one variant per row. Copy to clipboardAllegrois the live offer price, from Copy to clipboardallegro_offer.price_amountwith its Copy to clipboardprice_currency. It is a separate column rather than a second line inside the status badge, because the whole point of showing it is comparing it with the shop price and the SRP the kit renders two columns to the left, and that comparison needs a figure in a money column lined up with those, not a number inside a coloured badge. Priority 9 puts it immediately before the status column, so the three prices sit together. A price on a Copy to clipboardpausedor Copy to clipboardendedoffer is muted: the figure is real, but nobody can buy at it, and rendering it like a live price would read as current. A SKU with no offer, or an offer with no price observed yet, renders a muted Copy to clipboard-; never Copy to clipboard0, and never an error. Copy to clipboardAllegro statusis the mapping state. The cell names what is wrong with that one SKU: the conflict code (Copy to clipboardduplicate-sku, Copy to clipboardno-offer, ...) in red, Copy to clipboarddriftin orange, Allegro's own offer status in green when it is listed and healthy, Copy to clipboardunlinkedin grey, and a muted "not listed" when the SKU has no mapping at all. This is the real per-row status the banner could only approximate, and it does not cost this plugin a competing products list of its own. Both columns share one request per page. Copy to clipboardloadDataruns per row, so two columns over a 100-row page would be 200 single-SKU requests for the same table. Copy to clipboardsrc/admin/lib/offer-batch.tscoalesces every SKU asked for within a tick - React flushes all the cells' effects in one pass - into a single Copy to clipboard/admin/allegro/offers?skus=...call, de-duplicating the SKU the two columns both want. It deliberately keeps no cache across batches: a price is exactly the thing a sync changes underneath the operator, so a re-render has to be able to re-read it. Copy to clipboardprice_amountis a decimal string (Copy to clipboardmodel.text(), Allegro's own value verbatim), not a Medusa Copy to clipboardBigNumber, and Copy to clipboardresolveVariantOfferPricereads it as one - with Copy to clipboardNumberrather than Copy to clipboardNumber.parseFloat, so Copy to clipboard"365,31"is rejected instead of silently becoming Copy to clipboard365. An unreadable amount is Copy to clipboardnull, which renders as the dash; Copy to clipboard0is left meaning zero. It used to read Copy to clipboard"3 offers / 1 conflict", because an admin-kit row was a product and a product spans many SKUs - which told an operator that something was broken without telling them which SKU, the one thing they needed in order to act. Now a row is one variant with at most one offer. The registration moved from Copy to clipboardregisterProductColumnto Copy to clipboardregisterVariantColumnand the SKU roll-up in Copy to clipboardsrc/admin/libis gone: Copy to clipboardsummarizeOfferStatus/ Copy to clipboardformatOfferStatusbecame Copy to clipboardresolveVariantOffer/ Copy to clipboardformatVariantOffer/ Copy to clipboardvariantOfferColor. The status column's header also changed from Copy to clipboardAllegroto Copy to clipboardAllegro status, so that the price column can carry the plainer name next to Copy to clipboardShopand Copy to clipboardSRP. - Settings -> Allegro - the configuration and control home: the OAuth connection, the live writer toggles (interactive switches backed by the persisted runtime settings - arm or disarm each writer without a redeploy; a writer the environment forces off is shown locked), the sync configuration fields (editable inputs backed by the same singleton - see Sync configuration fields; a field an environment variable locks is shown locked, same treatment as a forced-off toggle), a catalogue roll-up, sync health, and links into the three nested Settings pages below.
- Settings -> Allegro -> Offers - the cross-catalogue offer table with conflict and drift filters, bulk rediscovery, and manual push. An operator triage surface for catalogue-wide "which offers are not syncing, and fix them" work - a genuine multi-item workflow the per-product widget cannot serve, and distinct from the browse case the admin-kit Catalog column covers.
- Settings -> Allegro -> Orders - the orders quarantine repair and import window. Operational task-flow, not a setting itself, but still nested here rather than in the main sidebar.
- Settings -> Allegro -> Category rates - the per-category sale commissions that set every price floor. Pure configuration, hand-maintained from Allegro's published fee table.
The sygnatura / SKU mapping principle
A Medusa variant and an Allegro offer are linked by SKU, and only by SKU.
Allegro lets a seller put their own identifier on every offer, in the field the API calls Copy to clipboardexternal.id and the seller panel calls sygnatura. This plugin's contract is that you put the Medusa variant SKU there. Offer discovery then matches Copy to clipboardexternal.id against variant SKUs, and Copy to clipboardallegro_offer.sku carries a unique constraint because it is the identity of the row.
Copy to clipboardallegro_offer.offer_id is a resolved cache, never the identity. Allegro offer ids are not stable across an item's life: re-listing an ended offer produces a new id, and one SKU legitimately moves between offers over time. A mapping keyed on the offer id turns every re-list into a silent orphan that stops receiving stock and price updates while still looking healthy. A mapping keyed on the SKU turns the same event into a row whose Copy to clipboardoffer_id needs re-resolving, which the next discovery pass does on its own.
Practical consequence: fill in the sygnatura on every Allegro offer you want managed. An offer without one is invisible to this plugin by design. That is the correct default - it means a seller can keep offers outside Medusa's control simply by leaving the field empty.
Data model
Table What it holds Copy to clipboardallegro_auth The OAuth connection. Both tokens AES-256-GCM encrypted, plus expiry, granted scope, and the account login. Copy to clipboardallegro_offer SKU-to-offer mapping. Copy to clipboardsku unique, Copy to clipboardoffer_id a resolved cache. Money as text, verbatim from Allegro. Copy to clipboardallegro_category_rate Sale commission per Allegro category, plain and promoted. Maintained by an operator - see below. Copy to clipboardallegro_price_push Append-only audit of every pricing decision: the rule and the pushed Copy to clipboard[floor, ceiling] in automation-rule mode, the exact Copy to clipboardprice_amount / Copy to clipboardprice_currency in fixed-price mode. Copy to clipboardallegro_order One row per Allegro checkout form: the Medusa order it produced, the raw and derived statuses, conflicts, and the attached invoice document. Copy to clipboardallegro_sync_state Per-loop health: status, cursor, counters, last error, failure state, the write-scope flag, and the claim's fencing token plus its heartbeat. Copy to clipboardallegro_settings The one-row singleton of persisted settings: the pricing mode, the sync configuration fields, and the runtime toggles - the live, operator-flippable arming of each writer. Writers default off, invoice-attach on.
Three of these carry non-obvious constraints worth knowing before you build on them.
Copy to clipboardallegro_price_push is append-only, and it is the only record of pushed price bounds. Allegro's API accepts a Copy to clipboard[min, max] price range when you attach a price-automation rule to an offer, and it will tell you afterwards which rule is attached - but it never returns the range. The bounds are write-only. So this table is the only place that can answer "what floor is this offer pinned to, and who set it". Never update or delete a row; correct a mistake by appending. Rows with Copy to clipboardresult: "observed" record state the plugin saw without touching, which is what makes a read-only monitoring pass worth running.
Copy to clipboardallegro_category_rate is filled in by hand, on purpose. Allegro does publish a fee calculator (Copy to clipboardPOST /pricing/offer-fee-preview, wrapped by the SDK as Copy to clipboardofferFeePreview), but in production it rejects the offer bodies you can build from a seller's own live offers, so sweeping a real catalogue returns errors rather than rates. Until that changes, an operator enters rates from the published fee table. Both rate columns are nullable so "unknown" stays distinguishable from "zero commission": a margin calculation that reads a missing rate as 0% quietly turns a loss-making price into an acceptable one.
Copy to clipboardallegro_order is separate from the Medusa order on purpose. A checkout form can exist without an order (creation failed, so the form stays visible with its error rather than vanishing), Allegro's status ladder is richer than Medusa's enum, and Copy to clipboardderived_status has to be the comparison basis for "did Allegro move?" - see Status mapping. Its two invoice columns follow the same write-last discipline: Copy to clipboardallegro_invoice_id is stamped when the document is registered and Copy to clipboardinvoice_attached_at only once Allegro has the file, so a row reading attached carries a PDF the buyer can download - see The invoice chain.
The sync architecture
Five loops, each with its own row in Copy to clipboardallegro_sync_state, its own single-flight claim, and - for the ones that write - its own kill switch. They are independently observable and independently runnable from the admin.
Two things write to Allegro outside the loops, both on a Medusa event: the fulfillment write-back and the invoice attach. Neither is a loop because neither has reconcilable state to compare - see Fulfillment write-back and The invoice chain.
Loop Provider Schedule Writes to Allegro? Offer discovery Copy to clipboardoffers Copy to clipboardALLEGRO_OFFER_SYNC_CRON, Copy to clipboard15 * * * * No Pricing monitor Copy to clipboardprice-automation chained after discovery No Price sync Copy to clipboardprices chained after the monitor Yes - price-automation command Stock push Copy to clipboardstock Copy to clipboardALLEGRO_STOCK_SYNC_CRON, Copy to clipboard*/15 * * * Yes - quantity-change command Order drain Copy to clipboardorders Copy to clipboardALLEGRO_ORDERS_SYNC_CRON, Copy to clipboard* * * * * Only fulfillment status, on an event
The first three are chained into one job rather than scheduled separately because they all need the same input - a complete listing of the seller's offers - and paging a full catalogue three times an hour is how a well-behaved integration earns a rate limit. The order matters: discovery establishes which offer owns which SKU and which mappings are conflicted, and price sync refuses to write to anything conflicted, so running price sync against a stale mapping is exactly the case where a command lands on the wrong offer.
Stock has its own cadence because stock moves on every order and an hour-stale marketplace quantity is how a sold-out item stays purchasable — and it also has an event-driven fast path on top, so a sale updates its own SKUs within seconds rather than waiting for the sweep. Orders runs on a ~20s interval because an unapplied Copy to clipboardBOUGHT event is an order nobody has been told about; an interval rather than a cron because Medusa's cron only resolves to the minute.
Reconciliation first, events almost never
Every loop except fulfillment write-back is a reconciliation: it reads the whole relevant state on each run and computes the difference. None of them depends on a Medusa event firing.
That is deliberate. Medusa's inventory events are not a reliable trigger (medusa#11691), and a design that depended on them would leave a permanently wrong marketplace quantity behind every missed event. With reconciliation, a missed event costs at most one cycle of staleness.
The one loop that is only event-driven is fulfillment write-back, and it is an exception for a structural reason rather than a convenient one: a fulfillment is a point-in-time act, not reconcilable state. There is no "current fulfillment status" in Medusa for a sweep to compare against Allegro's, so the event is the only signal there is.
Stock is the one place where events and reconciliation run together. Order and reservation lifecycle events mark the SKUs they touched dirty, and a debounced queue pushes just those offers within seconds; the 15-minute sweep still reads the whole catalogue and repairs anything the events missed. The events are a hint about what to re-read, never a source of quantity — the push reads Medusa's available quantity and Allegro's offer for itself — so the unreliability behind medusa#11691 cannot produce a wrong write, only a late one. And because the sweep is unchanged, a dropped event costs exactly what it cost before: the next cycle.
Medusa inventory is the source of truth for stock
The quantity pushed to Allegro is Copy to clipboardretrieveAvailableQuantity - stocked minus reserved, so units already promised to unfulfilled Medusa orders are not advertised again.
Keeping Medusa inventory honest is explicitly not this plugin's job. In this stack that belongs to a separate inventory plugin, which owns the supplier snapshot and the arming gate that refuses to propagate an untrustworthy one into Medusa inventory. That guard lives one layer up, where the supplier response is actually visible; a second one here would be a guess about data this plugin has no source for.
What this loop does refuse on is its own uncertainty, and the line is drawn at UNKNOWNS rather than at gaps. An ambiguous SKU match, or a quantity that could not be READ on either side, refuses the whole plan: a partial push in that state leaves some offers fresh and others stale with nothing recording which is which, so the next run cannot tell either.
A KNOWN, bounded exclusion does not refuse anything. Each is counted, reported in Copy to clipboardlast_error, and leaves exactly one offer alone: an inactive offer, a variant that does not manage inventory (so Medusa has no quantity to publish - a digital product, say), an offer that contradicts its mapping row, a mapped offer absent from the listing, an offer whose own Allegro listing carried no usable Copy to clipboardstock.available, and an eligible variant no mapped offer claims. Treating "this variant has no inventory" as an unknown is what previously let a single digital product with an Allegro offer refuse the entire catalogue's stock sync indefinitely.
A configured Copy to clipboardstockLocationIds is validated against the locations that exist, and an unknown id aborts the run. Medusa reports zero available quantity for a location that does not exi

