Medusa v2 plugin integrating Allegro, the Polish marketplace: OAuth connection, encrypted token storage, offer/SKU mapping and price-automation audit models.
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 .
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:
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 , and a fresh install starts its order cursor at "now" rather than importing history. See Runtime toggles and Turning the writers on.
is on npm:
That resolves to a prebuilt tarball - the published package already contains the output its point at, so nothing needs to compile on install. It pulls from the registry too, for the Catalog columns it contributes.
keeps moving after a release ships (see Releasing), so if you need a fix or feature that has landed on but not yet been released, install it as a git dependency instead, pinned to a commit:
Pin the commit you tested against. would move under you on the next push.
Installed that way, the package compiles itself on install - runs , which turns the checked-out source into the output its 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:
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 :
Then run the migrations:
npx medusa db:migrate| Option | Type | Required | Default | Notes |
|---|---|---|---|---|
| yes | - | Allegro application client id. | ||
| yes | - | Allegro application client secret. | ||
| no | Sandbox talks to . | |||
| yes | - | Must match the registered app name. No whitespace or HTTP separators. | ||
| yes | - | Your integration version, e.g. . | ||
| yes | - | Public http(s) URL documenting or contacting the integration. | ||
| yes | - | Base64-encoded 32 bytes. Seals the stored tokens. Rotating it makes existing tokens unreadable: reconnect after a rotation. | ||
| no | A rooted path on this backend, matching the redirect URI registered for the app character for character. is rejected: it is a protocol-relative URL, not a path. | |||
| no | Space-separated. Drop if you only ever want read access; the plugin then reports the missing write scope in the UI. | |||
| no | 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 for the env-driven incident case. | |||
| no | 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 . | |||
| no | Force-disable override for attaching invoice PDFs to Allegro orders. Its own switch, not a reading of - see The invoice chain. Same boolean-only contract. | |||
| no | derived | Absolute base URL of this backend. Set it when a proxy rewrites . Falls back to , then the request. |
| Option | Type | Required | Default | Notes |
|---|---|---|---|---|
| no | How this store prices its Allegro offers - see Pricing modes. This is the DEFAULT; the persisted admin choice wins over it. is what this plugin did before the mode existed, so an upgrade changes nothing. | |||
| 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. | ||
| no | Price-automation commands per run. Positive integer; 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. | |||
| no | Force-disable override for quantity writes. Can only force OFF; the live arming is the runtime toggle. Same boolean-only contract as . | |||
| no | 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. | |||
| no | - | Scopes which products are sync-eligible. With neither this nor , the whole catalogue is eligible. Also editable and persisted from the admin - wiring-critical, see Sync configuration fields. | ||
| 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. | ||
| no | every location | Locations whose available quantity is summed for the push. overrides it. | ||
| no | - | Reads the SRP (the price-range ceiling) from that key in the variant's , falling back to the product's. Mutually exclusive with . Also editable and persisted from the admin. | ||
| no | - | Reads the SRP from the variant's price in that price list. Also editable and persisted from the admin. | ||
| no | Container key of , resolved lazily and optionally. Without it, every offer is skipped with . There is never a default floor. | |||
| no | 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. | |||
| no | Marketplace the rule assignment targets. Also editable and persisted from the admin - wiring-critical, see Sync configuration fields. | |||
| 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:
| Variable | Effect |
|---|---|
| , or 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. | |
| The same, for quantity writes. alone does not stop all writes - the quantity command is a separate writer. | |
| The same, for the order drain. | |
| 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. | |
| The same, for attaching invoice PDFs. A separate switch from the drain on purpose - see The invoice chain. | |
| Schedule for the hourly catalogue pass (discovery, monitor, price sync). Default . | |
| Schedule for the quantity backstop. Default . Quantities normally reach Allegro through the event path (a sale, or ) within seconds; this sweep repairs what that missed - see for the exact list. | |
| Interval, in ms, for the order drain. Default (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. | |
| 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. | |
| Minimum gap, in ms, between reconciliation sweeps of orders with no registered payment. Default - 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. | |
| The same, for orders that are paid but not finished (awaiting shipment, in transit). Default (15 min): a lost event here delays a status label, not the money. | |
| Maximum orders re-read from Allegro per sweep. Default . 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. | |
| How long a shipped Medusa fulfillment is left to the subscriber before the sweep pushes itself. Default (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 followed by a that still read , correct only on a re-read ~45s later), so a sweep straight after a successful push would still see 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 only if you have disabled the subscriber. | |
| Comma-separated stock location ids, overriding . | |
| Locks the pricing mode, beating both the admin picker and . Ignored (read as unset) unless it names a real mode. See Pricing modes. | |
| Locks the standard-offer automation rule name, beating both the admin field and . See Sync configuration fields. | |
| The same, for the promoted-offer rule name. | |
| The same, for the SRP metadata key. | |
| The same, for the SRP price list id. | |
| The same, for the per-run change cap. Ignored (read as unset) unless it is a positive integer. | |
| The same, for the marketplace id. Wiring-critical - see Sync configuration fields. | |
| The same, for the sales-channel id. Wiring-critical. | |
| The same, for the sales-channel name. | |
| Fallback for 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 at plugin-load time, before the DI container - and therefore this plugin's - 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.
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 |
|---|---|
| (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. |
| (Allegro automation rule) | One per offer: attach the named rule its promotion state calls for, with as the rule's price range. Allegro's engine then picks the number inside that range. |
| (Fixed price from Medusa) | One 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. |
is the default, because it is what this plugin did before the mode existed. Upgrading changes nothing about what your store writes.
The break-even floor (from , 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:
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.
Two things are true and worth stating plainly before you switch a live store to it:
Both commands count as one offer against the per-run change cap.
The variant's own default price in Medusa, in the offer's own currency. Two rules, both fail-closed:
Every mode writes to the same append-only trail, but they fill different columns, and the difference is load-bearing:
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.
Every writer that reaches Allegro is governed by a persisted, operator-flippable toggle, stored as a one-row 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 | off | ||
| Quantity writes | off | ||
| Order drain | off | ||
| Fulfillment write-back | off | ||
| Invoice attach | on (inert) |
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:
effectiveEnabled = persistedEnabled && !forceDisabledSo:
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.
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.
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 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 , and falls through to the option.
| Field | Column | option | Env lock |
|---|---|---|---|
| Pricing mode | |||
| Automation rule (standard) | |||
| Automation rule (promoted) | |||
| SRP source: metadata key | |||
| SRP source: price list id | |||
| Change cap | |||
| Marketplace id (wiring-critical) | |||
| Sales channel id (wiring-critical) | |||
| Sales channel name |
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:
effectiveValue = envLock ?? persistedValue ?? medusaConfigDefaultClearing a field in the admin (blank it and Save) writes , which falls back to the option rather than to an empty value - the same "clear" contract the category-rates page already uses.
Editing or 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 or to pin the correct value against an admin mistake during a cutover. The admin renders an explicit warning on both inputs saying so.
Two invariants that already existed as boot-time checks in - 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 rather than silently accepted.
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: . This exact string goes into the option, because Allegro requires every request to carry a that identifies the registered app one-to-one. The plugin composes 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 :
https://your-medusa-backend.example.com/admin/allegro/oauth/callbackAllegro compares this byte for byte during the token exchange. A trailing slash difference is a failed connection.
Grant the app the scopes you configured: offer read, offer write, order read.
Copy the client id and secret into and .
Put it in . 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 never throws: it drops every character outside the alphabet, so a length test alone accepts mangled input, and decodes to a well-formed all-zero key. Both are rejected.
The key also signs the OAuth (see below), so rotating it invalidates any connection flow that is mid-air as well as the stored tokens.
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 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.
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.
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 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.
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 () - 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 (). That is an explicit exclusion - the page fetches the variant table separately with - so handed to a widget is . This widget derived its SKU list from and returned when that list was empty, which meant it rendered nothing, on every product, on every store. It now calls itself and still prefers if a future dashboard version passes it.
Product list banner () - 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 () - two columns registered into 's Catalog route, which lists one variant per row.
is the live offer price, from with its . 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 or offer 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 ; never , and never an error.
is the mapping state. The cell names what is wrong with that one SKU: the conflict code (, , ...) in red, in orange, Allegro's own offer status in green when it is listed and healthy, in 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. runs per row, so two columns over a 100-row page would be 200 single-SKU requests for the same table. coalesces every SKU asked for within a tick - React flushes all the cells' effects in one pass - into a single 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.
is a decimal string (, Allegro's own value verbatim), not a Medusa , and reads it as one - with rather than , so is rejected instead of silently becoming . An unreadable amount is , which renders as the dash; is left meaning zero.
It used to read , 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 to and the SKU roll-up in is gone: / became / / . The status column's header also changed from to , so that the price column can carry the plainer name next to and .
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.
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 and the seller panel calls sygnatura. This plugin's contract is that you put the Medusa variant SKU there. Offer discovery then matches against variant SKUs, and carries a unique constraint because it is the identity of the row.
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 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.
| Table | What it holds |
|---|---|
| The OAuth connection. Both tokens AES-256-GCM encrypted, plus expiry, granted scope, and the account login. | |
| SKU-to-offer mapping. unique, a resolved cache. Money as text, verbatim from Allegro. | |
| Sale commission per Allegro category, plain and promoted. Maintained by an operator - see below. | |
| Append-only audit of every pricing decision: the rule and the pushed in automation-rule mode, the exact / in fixed-price mode. | |
| One row per Allegro checkout form: the Medusa order it produced, the raw and derived statuses, conflicts, and the attached invoice document. | |
| Per-loop health: status, cursor, counters, last error, failure state, the write-scope flag, and the claim's fencing token plus its heartbeat. | |
| 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.
is append-only, and it is the only record of pushed price bounds. Allegro's API accepts a 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 record state the plugin saw without touching, which is what makes a read-only monitoring pass worth running.
is filled in by hand, on purpose. Allegro does publish a fee calculator (, wrapped by the SDK as ), 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.
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 has to be the comparison basis for "did Allegro move?" - see Status mapping. Its two invoice columns follow the same write-last discipline: is stamped when the document is registered and only once Allegro has the file, so a row reading attached carries a PDF the buyer can download - see The invoice chain.
Five loops, each with its own row in , 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 | , | No | |
| Pricing monitor | chained after discovery | No | |
| Price sync | chained after the monitor | Yes - price-automation command | |
| Stock push | , | Yes - quantity-change command | |
| Order drain | , | 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 event is an order nobody has been told about; an interval rather than a cron because Medusa's cron only resolves to the minute.
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.
The quantity pushed to Allegro is - 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 , 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 , 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 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
npm install @zanreal/medusa-allegro1// package.json2{3 "dependencies": {4 "@zanreal/medusa-allegro": "github:zanreal-labs/medusa-allegro#b0e864ab6a05e63e6d57a20b3c8adaf943049cdd"5 }6}1# pnpm-workspace.yaml2allowBuilds:3 "@zanreal/medusa-allegro@https://codeload.github.com/zanreal-labs/medusa-allegro/tar.gz/b0e864ab6a05e63e6d57a20b3c8adaf943049cdd": true1import { defineConfig } from "@medusajs/framework/utils";2
3module.exports = defineConfig({4 plugins: [5 {6 resolve: "@zanreal/medusa-allegro",7 options: {8 clientId: process.env.ALLEGRO_CLIENT_ID,9 clientSecret: process.env.ALLEGRO_CLIENT_SECRET,10 environment: process.env.ALLEGRO_ENVIRONMENT ?? "production",11
12 // App identity. `appName` must match the app registered in the Allegro13 // Developer Portal; Allegro rejects requests whose User-Agent does not14 // identify a real app.15 appName: "MyStoreAllegro",16 appVersion: "1.0.0",17 docsUrl: "https://mystore.example.com/integrations/allegro",18
19 // openssl rand -base64 3220 encryptionKey: process.env.ALLEGRO_ENCRYPTION_KEY,21 },22 },23 ],24});openssl rand -base64 32