smaily / smailyformagento
Smaily extension for Magento 2
Smaily Connect for Magento 2
Smaily email marketing, automations and Campaign
Intelligence for Magento 2, Adobe Commerce and Mage-OS. Feature-aligned
with the Smaily Connect plugins for WooCommerce and Shopify.
Features
- Contact synchronization — near-real-time, two-way: new contacts
flow to Smaily instantly; unsubscribes in Smaily mirror back to Magento. - Contact sync modes — lawful-basis presets: subscribers only (consent,
default), all customers (legitimate interest), or checkout opt-in only. - Marketing automations — welcome, first order and abandoned cart
events trigger your Smaily workflows, with per-language routing for
multilingual stores. - Abandoned cart — configurable cutoff, rich product payloads and a
secure recovery link that restores the exact cart. - Checkout opt-in — a newsletter checkbox in the checkout payment step
(guests and customers, double opt-in respected). - Product RSS feed — for the Smaily template editor, with category,
limit and sort parameters and storefront-accurate pricing. - Campaign Intelligence (optional) — catalog, customer, order and
browse data power personalized recommendations, attribution and
engine-run automations (replenishment, win-back, …); a CMS widget shows
each shopper their recommendations on the store's pages. - Operational visibility — durable delivery queues with automatic
retries, admin event logs with one-click retry, health notices, and
chunked historical imports that never block live traffic. - Privacy-first — encrypted credentials, GDPR export/erase tooling and
a shopper personalization opt-out page. - Translated — ships with English and Estonian (
et_EE) translation
packs for the admin and the storefront.
Requirements
- Magento Open Source / Adobe Commerce 2.4.4+ or Mage-OS
- PHP 8.1 – 8.4
- A working Magento cron (ideally every minute)
Installation
composer require smaily/smailyformagento:3.0.0-rc11
bin/magento module:enable Smaily_Connect
bin/magento setup:upgrade
Until 3.0.0 is released, require the release candidate's exact version, as
the releases page
names it: without a version, composer installs the old 2.8.1. A store in
production mode also needs maintenance mode, setup:di:compile and
setup:static-content:deploy — the full sequence is in the
User Guide.
Manual install: extract the release ZIP to app/code/Smaily/Connect and
run the same bin/magento commands. Each release also carries a .sha256
file next to the ZIP — sha256sum -c smaily-connect-magento2.zip.sha256
confirms you downloaded the archive we built.
Upgrading from 2.8.x? Settings migrate automatically, but
composer update alone stays on 2.8.x — follow the commands in
UPGRADING.md.
Documentation
| User Guide | Setup, every setting explained, CLI reference, FAQ — in English and Estonian |
| Installing from the ZIP | Manual install without composer: verify, extract, set up, update, remove |
| Upgrading | Migrating from Smaily for Magento 2.8.x |
| Architecture | How the module works inside (for developers) |
| Hyvä Support | Hyvä theme compatibility: audit, compat module, verification results |
| Headless Storefronts | What works with a separate storefront application, and what its team must add |
| Testing | Test suites, sandbox, upgrade verification |
| Contributing | Development environment and quality gates |
Quick start
- Run the wizard: Marketing > Smaily Connect opens the guided setup
on a fresh install — connect your Smaily account, choose your audience,
map automations and (optionally) Campaign Intelligence in five steps. - Everything after that: Marketing > Smaily Connect > Dashboard
(health and activity at a glance), Settings (the same options as
always-available tabs, including historical imports) and Log (every
delivery, with retry).
Everything is configured on the module's own pages — there is no separate
Stores > Configuration entry. Installs with more than one website get an
explicit website selector on Settings (and a website-picker step in the
wizard) so each website keeps its own connection and settings.
Development
composer install # Magento packages via the Mage-OS mirror
vendor/bin/phpunit --testsuite unit
vendor/bin/phpcs
vendor/bin/phpstan analyse
vendor/bin/phpunit -c phpunit.integration.xml.dist # needs MySQL, see TESTING.md
docker compose up -d # Magento 2.4.8 sandbox on http://localhost:8080
License
GPL-3.0 — see LICENSE.txt.
Legacy note: the 2.8.x extension (Smaily_SmailyForMagento) is no longer
developed. Its releases stay available under their tags, the last one
2.8.1.
Changelog
3.0.0 (unreleased)
The package version is currently 3.0.0-rc11 — the eleventh release-candidate cut of everything below. Release candidates are GitHub pre-releases for pilot stores, tagged in this repository from 3.0.0-rc9 on; composer installs one only when a store asks for that version or for a minimum stability of RC, and still resolves 2.8.1 as the newest stable release.
Changes since 3.0.0-rc10
- Details in Marketing > Smaily Connect > Log shows when a row's latest attempt happened: the time of its latest failure, its delivery or its skip. The Log's Updated column and the Dashboard's Recent activity show the same time. Before, a row that failed, was delivered or was skipped kept the time of its previous change — often the time it was queued. The banners and the admin notification about deliveries that failed in the last 24 hours now count failures by when they happened, and the Log keeps a delivered or failed row for its retention period counted from that time. Rows already in the Log keep the time they show until their next attempt.
- The extension's log (
var/log/smaily_connect.log) records Campaign Intelligence rows it gives up on as it records contact syncs and automations since 3.0.0-rc10: when the engine refuses a whole batch, or a batch fails for one reason, the rows it parks get one line, "Ingest events failed permanently", with their number and ids, instead of one "Ingest event failed permanently" line per row. A row given up on alone now gets the same line, with a number of 1 and its id — "Ingest events failed permanently", or "Queue events failed permanently" for a contact sync or an automation — instead of "Ingest event failed permanently" or "Queue event failed permanently". Marketing > Smaily Connect > Log shows each row's status, attempts and error as before. - A shopper's sent and failed rows in Marketing > Smaily Connect > Log that
bin/magento smaily:gdpr eraseanonymizes keep the time of their latest attempt, so they are removed on their normal schedule: 30 days after the delivery, 90 days after the failure. Before, the erasure restarted that period, and the rows stayed up to 30 or 90 days longer. - Connecting Campaign Intelligence — Connect on Settings > Intelligence or in the initial setup, or
bin/magento config:set smaily_connect/intelligence/setup_token— now starts the Customers import too, after the catalog import: your existing customer accounts go to Campaign Intelligence once, so that it knows every customer by their Magento customer account, also one it knew only from orders or one created by Magento's own customer import. A notice says "The customers import has started" (EN + ET); Cancel import on the Customers card under Settings > Intelligence > Historical imports holds it back. A customers import already queued or running is left as it is. Before, connecting started only the catalog import. The Orders import is still yours to start. On a store connected before this release, press Start import on the Customers card if that import has never run. - New: the Smaily recommendations widget shows each shopper their own Campaign Intelligence recommendations — the products in their Smaily emails — on your store's pages, under "Recommended for you" ("Sulle soovitatud"), as your theme's own product cards. Place it on a CMS page or block with Insert Widget... > Smaily recommendations (in Page Builder, in a Text element); without it nothing changes, and it has no settings. A signed-in customer sees theirs unless they opted out of personalized recommendations or unsubscribed; a guest only after an earlier click on a Smaily email link; no shopper without marketing consent, which is read as for the browse tracker. Up to four products, with the store's own name, image and price; a product that is not visible in the catalog or not for sale is left out, and with nothing to show nothing appears, not even the heading. A purchase after a click on a card is credited to your store's pages. The page stays in the full-page cache: the cards load after the page, and a shopper's recommendations are kept for an hour. A Hyvä store needs the Hyvä compatibility module; a separate storefront does not get the widget. No catalog import is needed.
- A shopper's abandoned-cart records that
bin/magento smaily:gdpr erasemarks as erased keep the time of their last change, and stay while the shopper's cart is still open in the store, so no reminder goes to the erased address if that cart changes again later. Once the cart is ordered or closed, or Magento deletes it, the nightly tidy-up removes the record 30 days after its last change. Before, the erasure restarted the 30 days, and the tidy-up could remove the record after 30 days while the cart was still open. - Upgrading from 2.8.x: each website's welcome automation is on exactly where 2.8.x sent the opt-in email. A website with Enable Subscribers Collection off under a Default Config with it on keeps the welcome automation off — before, the upgrade switched it on there — and a website with it on under a Default Config with it off keeps it on with the Default Config Autoresponder ID.
- On a store with more than one website, picking a website that has not finished the initial setup in the Website selector on Settings now opens the initial setup of that website. Before, the initial setup opened on Which website are you setting up? with the main website chosen, so you could set up the wrong website. A link to Dashboard or Log for such a website does the same.
- The initial setup's Contacts step no longer switches contact sync on for a website where it is off (for example after an upgrade from 2.8.x with Enable Module = No): the step now shows Sync contacts to Smaily unticked there, Continue keeps sync off, and ticking it switches sync on. A website whose contact sync is on sees the step as before.
- Upgrading from 2.8.x: each website's abandoned-cart automation is on exactly where 2.8.x sent the reminder. A website with Autoresponder ID set to No automation workflow selected under a Default Config with a workflow keeps the automation off — before, the upgrade gave it the Default Config workflow and reminders started there — and Enable Abandoned Cart without an Autoresponder ID anywhere carries over as off.
- The user guide links in the admin — the initial setup, the "Smaily Connect is ready to set up" notice and the cookie consent notes — open the new bilingual (English and Estonian) user guide at smaily.com/connect-magento, and so does the README. A notice an earlier version added keeps its old link, and the old guide page in the GitHub repository now points to the new one; finishing the initial setup still marks that notice read.
- A store connected to Campaign Intelligence whose customers import has never completed — connected before connecting started it, or with the import canceled or failed — now sees a system message at the top of every admin page, Smaily Connect: the customers import has never completed…, until one completes. Its link Start the customers import opens Settings > Intelligence, where Start import (or Run again) on the Customers card starts it; nothing starts it for you, and the message stays hidden while a customers import is queued or running, unless the card shows it as Stalled.
- The composer commands in the user guide and the 2.8.x upgrade guide now install version 3 as written: until 3.0.0 is released, they require the release candidate's exact version (
composer require smaily/smailyformagento:3.0.0-rc11), because without a version composer installs the old 2.8.1 and^3.0installs no release candidate on a standard Magento project. The user guide's production-mode install now includes maintenance mode andsetup:static-content:deploy, so the storefront and admin pages answer normally afterwards, as the release ZIP steps in INSTALLING.md already did. The README gives the same version. - Upgrading from 2.8.x: a website where 2.8.x had Enable Subscribers Collection or Enable Abandoned Cart on with no Autoresponder ID sent no email for it, and that automation stays off. An admin notice now names those websites — Smaily Connect upgrade: the welcome automation has no workflow or Smaily Connect upgrade: the abandoned-cart automation has no workflow — so you can tick Enabled for Welcome or Abandoned cart and pick a Smaily Workflow on the initial setup's Automations step.
- The Hyvä compatibility module's install steps now work as written: the module is not on Packagist yet and is not in the Smaily Connect package or the release ZIP, so the user guide's Hyvä section and the module's README take it from a git clone of the Smaily Connect version you install, then install it from a folder in your project (composer installs) or from
app/code/Hyva/SmailyConnect(release ZIP installs). The earlier steps pointed composer at a folder the package does not contain and stopped with an error.
Changes since 3.0.0-rc9
- An order created in the admin under Sales > Orders > Create New Order, or placed with Login as Customer, no longer counts as an order through the store's own pages when the extension decides whether to open Using a separate storefront? by itself. Before, on a store whose shoppers buy only on a separate storefront, one such order by the staff kept the Storefront URL field collapsed for 30 days. A shopper's order on the store's own pages, and an order through Magento's API, count as before.
- Two admin texts now read the same in English and Estonian: an import card that stopped before an error tells you to press Run again, the button the card shows (the English text said "the import button"), and the Estonian setup notice names the menu Turundus > Smaily Connect > Algseadistus, as the Estonian admin shows it, instead of Marketing > …. The setup notice is written once, at install, so a store that has it already keeps the earlier wording.
- A stalled import holds up less. A catalog import set aside as Stalled no longer keeps the nightly product list from going to Campaign Intelligence while other imports run behind it; before, the list waited until those imports had finished. An import that fails before its first page — for example while it counts the products, contacts or orders it will send — now counts as started, so after an hour it is set aside like any other stalled import and the imports behind it run; before, it stayed Pending and every import behind it waited until you pressed Run again or Cancel import on its card.
- The contacts import card (Settings > Contacts, and the initial setup's Contacts step) states before you start it how many contacts the import sends from the website: "This import sends 42 contacts from this website." (EN + ET), counted for the mode picked on the panel, as the import picks them. Before, the same sentence also named the store's orders and products ("Your store has 42 contacts to import, 1200 orders and 300 products. …"), which this import does not send; the order and product imports have their own cards under Settings > Intelligence, which are unchanged.
bin/magento smaily:backfill:start contactsfor a website whose Sync contacts to Smaily is off still starts the import, which sends nothing, and now says so: "Contact synchronization is off for website 2, so this import sends nothing and finishes at 0. Turn on Sync contacts to Smaily under Settings > Contacts, then start the import again." Before, it printed only that the import had started. - When Smaily refuses a whole group of contact syncs or automations with one answer, the extension's log (
var/log/smaily_connect.log) records the rows it gives up on in one line, "Queue events failed permanently", with their number and ids, instead of one "Queue event failed permanently" line per row. Marketing > Smaily Connect > Log shows each row's status, attempts and error as before.
Changes since 3.0.0-rc8
- An import that has stalled no longer holds up the imports queued behind it. A running import that nothing has moved for over an hour — for example one whose run fails on the same products every time — is set aside: the imports behind it run, its card keeps showing Stalled with Run again and Cancel import, and it is tried again whenever no other import is waiting. Before, every import queued behind it waited until you pressed Run again or Cancel import on its card, and their cards named it as the import they waited behind. An import still queued is never set aside: when the import at the front of the line never gets going — for example because it fails before its first page or because cron does not run — the imports behind it still wait, and their cards still name it.
- An order created in the admin under Sales > Orders > Create New Order reaches Campaign Intelligence without the browsing and recommendation clicks of the admin's own browser, as an order placed with Login as Customer does. Before, on a store whose admin shares the storefront's address, the order could carry the session, visitor token, recommendation id and context of the admin's browser when that browser had visited the store. A shopper's own order carries them as before.
- The extension's home is the official repository, sendsmaily/smaily-magento-extension: every user guide link in the admin (among them the initial setup and the "Smaily Connect is ready to set up" notice) and in the README points there, and so do the releases. A "ready to set up" notice that an earlier release candidate added keeps its old link, and finishing the initial setup still marks it read; uninstalling removes it.
Changes since 3.0.0-rc7
- Products deleted with Magento's import — System > Data Transfer > Import with the Delete behaviour, or a tool that runs Magento's import — are removed from Campaign Intelligence, as a product deleted in the admin is: in the Log, a catalog_remove row per product, and for a variant of a configurable product a catalog row that marks it out of stock. Before, the import sent nothing, so Campaign Intelligence kept recommending the deleted products, and a catalog import did not remove them. A product deleted straight in the database, or by a tool that bypasses Magento's import, still sends nothing at once: the nightly product list (below) takes it out of the recommendations the next night, and the user guide says how to take it out at once.
- The separate-storefront note on Settings > Intelligence ("Using a separate storefront? Set its Storefront URL under Settings > Connection …") follows the Storefront URL without a reload: saving a Storefront URL on Settings > Connection hides it, and clearing the Storefront URL and saving shows it again. Before, the note stayed until the page was reloaded, although the Storefront URL was saved.
- When Smaily answers "invalid data" (code 203) for a group of contact syncs, the contacts of that group are sent again one at a time in the same run: the valid contacts sync, and only the contact Smaily refused fails in the Log, with Smaily's answer. Before, Smaily's one answer for the group failed every contact in it, so one invalid contact stopped up to 200 valid ones until each was retried by hand. The contacts import does the same with a chunk Smaily answers that way, so only the refused contacts count as failed. Smaily answers a whole request with one code, so this costs one extra request per contact of the group, and only on that answer; WooCommerce sends one contact per request, with the same outcome.
- While Smaily has deactivated the Campaign Intelligence account, Details on a waiting row bound for Campaign Intelligence — the catalog, customer, order and browsing data, a link of a shopper's browsing to their account (engine.identity_merge) and a personalization choice (engine.profiling_consent) — says "Waiting for your Campaign Intelligence account to be active again; this row is sent then." (EN + ET). Before, it said the row waited for the next queue flush, which runs every minute, or named the time of its next automatic retry, although nothing is sent until the account is active again.
- A contact sync or automation row in the Log for an email address longer than 64 characters no longer stores the address cut after 64 characters as its Entity: the Entity is a keyed hash of the address, as for a personalization choice, and the Log and the Dashboard show its first 12 characters (the Entity filter finds the row by them); Details shows the address in the payload. Before, the cut address did not match the full one, so when such a shopper bought, a waiting abandoned-cart reminder was not withdrawn and could still go out, and after a reminder had gone out the purchase marker was not sent. An address of 64 characters or fewer is shown and found by the Entity filter as before. Rows queued before keep the cut address until the Log's retention removes them;
bin/magento smaily:gdpr erasefinds those rows by their payload. - The abandoned-cart reminder writes the
over_10_productstemplate field on every reminder: "true" when the cart has more than 10 products, as before, and empty when it has 10 or fewer, as the unused product slots are sent. Before, the field was sent only for a cart of more than 10 products, so after such a cart the Smaily contact kept "true" and the next reminder, for a smaller cart, could still show a template's "and more products" part. - A credit memo that refunds no money — for example the return of a product that was free with the order, or one whose adjustment cancels the refund — now sends the order to Campaign Intelligence with the returned product marked as returned. Before, such a credit memo moved neither the order's status nor its refunded total, so the return reached Campaign Intelligence only with some later change to the order. A refund made through Magento's REST API (
POST /V1/order/{id}/refund,POST /V1/invoice/{id}/refund) now carries its returned products at once too; before, Magento saved the order before the credit memo, so the order went out without them. A refund still sends the order once (one orders row in the Log), also when it closes the order. - The Campaign Intelligence engine wire contract is v1.12.0. It adds options a store may use: recommendations shown on the store's own pages, a visitor token the store creates at checkout, and a nightly list of the store's products with which Campaign Intelligence removes products the store deleted without telling it. The extension does not show recommendations on the store's own pages yet; the store-created visitor token and the nightly list are below. It retires two preview addresses, which the extension never called. The contract sync itself changes nothing the extension sends, and no catalog import is needed.
- A credit memo that is canceled, or still pending, no longer marks its products as returned in the order sent to Campaign Intelligence; only a refunded credit memo does. Before, every credit memo of the order counted, whatever its state, so Campaign Intelligence could treat a product the customer kept as returned and stop recommending it to them. Magento's own refunds always create refunded credit memos, so this concerns credit memos that an extension, for example a payment or returns extension, leaves pending or cancels. Canceling a credit memo does not always send the order again by itself: an order already sent keeps the return until its next change, or until an order import under Settings > Intelligence > Historical imports sends it again.
- The initial setup's Connect step has the Storefront URL field (under Using a separate storefront?, below the account fields), checked as on Settings > Connection: fill it in only if shoppers buy on a separate (headless) storefront. It is saved with the connection, so a store with a separate storefront has its storefront's address before the Contacts step switches contact sync on and before the Campaign Intelligence step connects — no more finishing the setup without connecting and setting the address under Settings first. The Campaign Intelligence step's separate-storefront note now points back to the Connect step (EN + ET). In the setup reopened after it was finished, a Connect-step save that changes the Storefront URL while Campaign Intelligence is connected asks you to run the catalog import again, so that Campaign Intelligence gets the new product links, in the same words as Settings > Connection, beside the next step's Continue, and the setup's completed summary lists the Storefront URL when one is saved. Left empty, the setup runs as before.
- Once a night, at 03:30 store time, the extension sends Campaign Intelligence the list of every enabled product with its stock — the product code and whether it is in stock, nothing else. Campaign Intelligence stops recommending a product missing from the list and takes the list's stock where its own differs, so a product deleted straight in the database, or a stock change or deletion that did not arrive, is corrected by the next morning; before, such a product could stay in recommendations until the merchant ran a catalog import, and a deletion not even then. Disabled products are left out of the list, so they are not recommended either. A list that would remove more than a fifth of the products removes nothing until Smaily's team has checked it. Each night the list is sent is one catalog_manifest row in the Log; Details shows Campaign Intelligence's answer (products removed, stock values corrected, products it does not have, and whether its safety check held the removals back). The list is not sent while Campaign Intelligence is not connected or the account is deactivated, while the catalog import runs or waits to start, or while product changes still wait to be sent; it goes the next night. A store with more than 50,000 enabled products sends no list: its Log row fails with "Not sent: the store has more than 50,000 enabled products, the most the nightly product list can hold." (EN + ET). A list that could not be delivered is not sent again; the next night sends a new one. No catalog import is needed for this change.
- On a store with a separate (headless) storefront, a shopper who logs in through Magento's API — GraphQL
generateCustomerTokenor RESTintegration/customer/token— now has the browsing they did before the login linked to their account in Campaign Intelligence (an engine.identity_merge row in the Log), as a login on Magento's own pages does. The storefront's login request has to carry thesmaily_anon_sidorsmaily_rec_uidcookie (both when it has them), as its place-order request carries the attribution cookies; the separate-storefront guide says how. Before, such a login linked nothing, even with the cookies. A shopper who turned profiling off is still not linked. Logins on Magento's own pages are unchanged. - A click on a recommendation link that names no context — for example a link from an older email — now clears the context an earlier click left in the shopper's browser, in Magento's own storefront and on Hyvä, as Campaign Intelligence's attribution rules require. Before, the earlier context stayed with the new click, so Campaign Intelligence could credit the purchase under the earlier click's context — once recommendations on the store's own pages are clicked, to those pages instead of the email the shopper clicked last. A page opened without a recommendation link leaves the earlier click as it is. A separate storefront needs the same change in its own capture; the separate-storefront guide states the rule.
- A visitor token that a store creates at checkout (
vs_followed by 22 letters and digits, the second form Campaign Intelligence's rules allow besides its ownvt_tokens from email links) is now sent with the order unchanged when the shopper's browser holds it, so Campaign Intelligence can recognise that shopper on a later visit. Before, the extension dropped such a token as malformed and sent the order without it. The extension does not create these tokens itself; a separate storefront or a customization that does is now supported. A value of neither form is still left off the order. - On a store with a Storefront URL saved, the Dashboard's Browse tracking card says "Your separate storefront must send the events itself" while browse tracking is on, and the note under the browse-tracking setting (Settings > Intelligence, and the initial setup's Intelligence step) says the separate storefront's own cookie consent banner decides (EN + ET). Before, the card said "Script live on storefront" and the note asked for Magento's cookie restriction mode, which describe Magento's own pages, not the separate storefront. The note follows a Storefront URL saved on Settings > Connection, or on the initial setup's Connect step, without a reload: saved, it is the separate storefront's; cleared, it is Magento's again; a refused save leaves it as it was. The admin notification asking for a consent source no longer comes for a store view with a Storefront URL saved; it still comes for a store view without one where cookie restriction mode is off. Without a Storefront URL, everything reads as before.
- When no nightly product list has gone to Campaign Intelligence for three nights in a row, the Dashboard says so: the verdict needs attention, and a warning banner says for how many nights and why the last night's list did not go out — the catalog import was running, product changes were waiting in the Log, the list could not be built, the store has more than 50,000 enabled products, or Campaign Intelligence did not take it (EN + ET). Before, a skipped night was written only to
var/log/smaily_connect.log, so a list skipped every night went unnoticed. The banner goes once a list is sent; a store that connected Campaign Intelligence less than three nights ago does not see it. - An import card (Settings > Intelligence > Historical imports, and the contacts import) whose import is queued or running but that nothing has moved for over an hour now shows Stalled, how far it got, and Run again, which cancels the stalled import and starts a fresh one (EN + ET); if it stalls again, check that Magento's cron runs. A start from the command line (
bin/magento smaily:backfill:start) and the catalog import that connecting Campaign Intelligence starts do the same: they cancel a stalled import of their kind and start a fresh one. An import that waits behind another, stalled import says so on its card and names that import's card — for example Settings > Contacts > Initial contact import — where Cancel import or Run again clears the way (EN + ET); it offers only Cancel import itself. Before, an import whose run failed on the same page every time, or one queued on a store whose cron did not run, showed Running or Pending forever, and starting it again was blocked because it still counted as running: the command said the import was already running, connecting started no catalog import, and only Cancel import first, then Run again, got it going. An import that moves shows its progress as before. - An admin who opens a customer's account with Magento's Login as Customer no longer links the browsing in their own browser to that customer in Campaign Intelligence. Before, the login queued an engine.identity_merge row from the admin's browsing cookies, when the admin's browser had visited the store. A shopper's own login links their browsing as before on Magento's pages, and through the GraphQL or REST customer token as described above. Stores without Login as Customer are unchanged.
- An order an admin places with Magento's Login as Customer reaches Campaign Intelligence without the browsing and recommendation clicks of the admin's own browser, so the admin's clicks credit no recommendation with it. Before, the order carried that browser's session, visitor token, recommendation id and context, as a shopper's own order does. A shopper's own order carries them as before. Stores without Login as Customer are unchanged.
Changes since 3.0.0-rc6
- The Log's Last Error filter finds rows by the text the column shows. Before, it searched the stored value: a refusal's internal failure class (
permanent_http_400: …), which the column does not show, could be found, and an extension message shown in the admin's language (for example in Estonian) could not be found by the words on screen. An error that the column shows with its secrets redacted is not found by the hidden values. - Details on a delivered automation row reads as delivered. When a later message of the same kind had reached the same contact, it said "A later message of this kind already reached this contact; sending again would deliver it twice." although the row was delivered and never offered Send again. That sentence now shows only on a failed row, where it explains why Send again is missing.
- Retry on a large Log selection (Select all) no longer loads every selected row first: it took about 45 MB of memory per 20,000 rows and could fail on a very large Log. It now retries the selection 1,000 rows at a time, in about 1.5 MB. The rows retried and the rows skipped as not safe to send again are the same as before.
- The Log's error column shows Smaily's answer to an HTTP error after the status, as it does for Campaign Intelligence: "Smaily API request failed with HTTP 400: <Smaily's message>" (EN + ET), the answer cut after 500 characters. Before, it showed only the status, and Smaily's answer only in Details. Rejected API credentials still read "Smaily API credentials were rejected", and a package without API access keeps its own sentence too.
- While Smaily has deactivated the Campaign Intelligence account, Settings > Automations shows the same explanation as Settings > Intelligence in place of the Campaign Intelligence automations, which cannot be read or saved meanwhile ("Your Campaign Intelligence account is not active, so its automations cannot be read or saved. Once Smaily tells you the account is active, press Check again under Settings > Intelligence."; EN + ET), and a save from a page opened earlier says the same and sends nothing. Before, the tab said it could not load the automation catalog and quoted the engine's refusal, and a save quoted it too.
bin/magento smaily:gdpr export|eraseno longer asks Campaign Intelligence then: it exports or erases the store's own data as before and says that the Campaign Intelligence data was not exported or erased because the account is not active, and what to do; it exits with an error, so a script notices. - A Smaily Log row that can never be delivered fails at once, with its reason, instead of after five attempts over about an hour and twenty minutes: a link of a shopper's browsing to their account at login (engine.identity_merge) that Campaign Intelligence refuses (an HTTP 4xx other than 429, shown with the failure class
permanent_http_<code>in Details, as a refused Smaily delivery already was), and a row whose stored data is incomplete or that no part of the extension handles. A row of an event type no part of the extension handles failed at once before too, but Details said all five attempts were used up; it now says it stopped after 1 of 5. Retry in the Log sends any of these again, as before. - While Smaily has deactivated the Campaign Intelligence account, the links of shoppers' browsing to their accounts at login (engine.identity_merge rows in the Log) wait, as the catalog, customer and order data already did, and are sent once the account is active again. Before, each such row failed with "Campaign Intelligence account is not active" and used up its five attempts.
- A contact sync or automation that Smaily answers with "invalid data" (code 203) fails at once in the Log, with Smaily's answer in the error column and the failure class
permanent_envelope_203in Details, instead of being sent four more times with the same data. Any other Smaily error code is retried as before. Retry in the Log sends such a row again once the data is fixed. - While Smaily has deactivated the Campaign Intelligence account, a shopper's personalization choice (an opt-out of personalized recommendations or opting back in; engine.profiling_consent rows in the Log) waits and is sent once the account is active again; when the shopper changed their mind meanwhile, the engine still gets only the newest choice. Before, each such row failed with "Campaign Intelligence account is not active" and used up its five attempts, so the choice could be lost. A choice Campaign Intelligence refuses (an HTTP 4xx other than 429) now fails at once with the engine's answer and the failure class
permanent_http_<code>in Details, instead of being sent four more times; an address the engine does not know (HTTP 404) still closes the row as delivered, since there is nothing to exclude. - A shopper's opt-out of personalized recommendations made before Campaign Intelligence knew the shopper — for example by a guest who only subscribed to the newsletter — now reaches Campaign Intelligence once it knows them. Campaign Intelligence answered such an opt-out "not found" and the row closed as delivered, so when a later order, a customer account save or the customer or order import created the shopper there, the shopper was personalized although the store's record said opted out. Now, when Campaign Intelligence confirms a customer or an order of a shopper who opted out, the opt-out is sent again (a new engine.profiling_consent row in the Log). A shopper who opted back in meanwhile, or never opted out, gets no such row; a shopper with many orders gets one waiting row, not one per order.
- A shopper's personalization choice in the queue (engine.profiling_consent rows in the Log) no longer stores the shopper's address as the row's Entity: the Entity is a keyed hash of the address, the one the store's record of opt-outs already uses, and the Log and the Dashboard show its first 12 characters (the Entity filter finds the row by them); Details shows the address in the payload, as before. Before, the Entity was the address itself, cut after 64 characters, so a shopper with a longer address got one more waiting opt-out for each confirmed order instead of one. Rows queued before keep the address until the Log's retention removes them (30 days after sending, 90 after failing);
bin/magento smaily:gdpr erasefinds and erases both kinds. - The Campaign Intelligence engine wire contract is v1.8.3: it states the catalog sync the extension already follows — the whole catalog once, when you connect, then only the changes, and a catalog import whenever you start one; no scheduled full re-sync. A catalog import does not remove products deleted outside Magento's own product delete. What the extension sends is unchanged.
Changes since 3.0.0-rc5
- On a store with Use Web Server Rewrites off, connecting Campaign Intelligence from the command line sent the engine a store address with
magentoin place of the storefront'sindex.php; the engine keeps that address in its audit log. It now sends the storefront's address, as a connection from the admin does. With rewrites on, the address is unchanged. - The nightly full catalog re-sync to Campaign Intelligence (03:40 store time) is removed, as in the WooCommerce plugin: Campaign Intelligence gets the whole catalog once, from the catalog import, and after that the changes — product saves, stock changes and deletions, as before. Connecting starts the catalog import (see below). A store that changes products outside Magento's own product save — an ERP link, a CSV or
bin/magento importrun, a direct database import — starts the catalog import by hand after such a change; before, the nightly re-sync sent those changes within a day. - Connecting Campaign Intelligence starts the catalog import, so Campaign Intelligence gets the whole catalog without a further click. The connection's result shows a notice, "The catalog import has started" (EN + ET), with Hold back the import: pressed before the next cron run picks the import up (usually within a minute), nothing is sent; pressed later, the products already queued for sending still reach Campaign Intelligence and the rest are not sent, and the notice says how many were queued. The import can be started again any time under Settings > Intelligence > Historical imports. Connecting again while a catalog import is queued or running starts no second one. A connection from the command line (
bin/magento config:set smaily_connect/intelligence/setup_token) starts it too; the command prints no notice, so check it withbin/magento smaily:backfill:statusand hold it back with Cancel import on the Catalog card. The customer and order imports are still started by hand. - The initial setup's Campaign Intelligence step tells a store with a separate (headless) storefront to set its Storefront URL before connecting, since connecting starts the catalog import at once and the Storefront URL is set under Settings > Connection, which opens once the setup is finished. While no Storefront URL is saved for the website, a note under the setup URL field says (EN + ET): finish the setup without connecting, enter the address under Settings > Connection > Using a separate storefront? > Storefront URL, then connect under Settings > Intelligence — or connect now and press Hold back the import. Settings > Intelligence, which connects too, shows the same note before Connect, pointing to Settings > Connection > Using a separate storefront? (EN + ET). With a Storefront URL saved, and once connected, the note is not shown.
- The Campaign Intelligence catalog, customer and order imports do not start while Campaign Intelligence is not connected: Start import on a Settings > Intelligence page opened before Campaign Intelligence was disconnected, and
bin/magento smaily:backfill:start catalog|customers|orders, say "Campaign Intelligence is not connected, so there is nowhere to send the catalog." (for customers: "…the customer data.", for orders: "…the order data."; EN + ET) and start nothing. Before, such a catalog import ran and finished with every product counted as failed, and a customer or order import queued its data with nowhere to send it. The contacts import to Smaily is unchanged.
Changes since 3.0.0-rc4
- A stock change no longer builds the product's Campaign Intelligence catalog entry inside the shipment, credit memo, order or inventory save that made it: the save records which products changed (one lookup and one insert, however many lines or products), and the catalog sync builds and sends their entries within the next minute, as before. A shipment of many lines and a bulk inventory update finish faster. What is sent is unchanged; in the Log, a stock change shows as a waiting catalog_changed row until its catalog row replaces it, and several stock changes of one product within a minute send one catalog row.
- Image and product links in the catalog sent to Campaign Intelligence open on the storefront. The catalog import, the nightly catalog re-sync and a product save in the admin sent each product's image link as a placeholder link that did not open, also for a product with an image; a product now gets the image its storefront shows, and a product without an image the storefront's placeholder image. On a store with Use Web Server Rewrites off, the product link from the catalog import and the nightly re-sync had
magentoin place of the storefront'sindex.phpand did not open; it is now the storefront's link. The nightly catalog re-sync corrects the entries already sent; a catalog import corrects them at once. - On a store with Use Web Server Rewrites off, the cart link in abandoned-cart reminders (
{{abandoned_cart_url}}) and the store link ({{store_url}}) hadmagentoin place of the storefront'sindex.php, so the cart link did not open the shopper's cart; both are now the storefront's links. With rewrites on, the links are unchanged. A reminder already sent keeps its link. - On a busy store that keeps its old carts, the abandoned-cart check no longer reads the whole cart table: once more than a few thousand carts changed in the last 24 hours, each check (every 5 minutes, per website) read every cart the store ever had — about 1.8 seconds on a store with 3 million carts. It now reads only the carts changed in the last 24 hours (about 50–100 ms with 60,000 of them). The same carts are reminded, skipped and left out as before.
- Campaign Intelligence automations show the state the engine stored after a save. Real sends are switched on by Smaily after the merchant confirms: a trigger saved with Test mode off stays in test mode until then, and its card now shows Test mode with the box ticked again, instead of keeping the merchant's choice on screen. Each card that is not Active says that Smaily switches real sends on after you confirm (EN + ET). The engine wire contract copy is synced (v1.8.2, the rule that real sends are switched on engine-side); what the extension sends is unchanged.
Changes since 3.0.0-rc3
- Campaign Intelligence gets a category for every variant: a variant of a configurable product that has no category of its own is sent with its parent product's category. Before, such a variant was sent as uncategorized. A variant with a category of its own keeps it.
- A purchase of a configurable product whose order line has no SKU is sent to Campaign Intelligence as the variant that was bought — its SKU, or its catalog id — so the purchase line names the same product as the variant's catalog entry. Before, such a line named the parent product. A line with a SKU, and a bundle, are unchanged.
- Switching from per-language Smaily accounts back to one account keeps a working account: the single-account fields can show the default store view's per-language account, and saved with an empty password, that account went to the whole website with the default fallback account's password, so Smaily refused it and syncing stopped. An empty password now keeps the saved one only for the account saved for the website; for any other account the save is refused on the password field until its password is entered (EN + ET).
- With per-language Smaily accounts, the connection status describes the account saved for the website — the default fallback account — after a save and after a reload alike: the save checks that account with its own password, and Settings > Connection, the finished initial setup's summary and the Dashboard describe it and name its subdomain. Before, the check and the status could take the website's default store view's account or password, so with that store view in another language than the fallback, or right after switching back to one account, the status could say Not connected although the saved accounts worked, or Connected after the save and Not connected after a reload.
- With per-language Smaily accounts, picking another default fallback account needs that account's password, on the server as well as in the browser: a save that skipped the form's check stored the new fallback account for the whole website with the old fallback account's password. Such a save is now refused on the new fallback account's password field (EN + ET). The Connection form asks for a password exactly when the save needs one: it compares with the account saved for the website, as the save does, so it no longer asks when the picked fallback is already the website's account, and no longer lets through the single account the fields show after leaving per-language accounts when that is not the website's account.
- On an installation with several websites, each website keeps its own default fallback language for per-language Smaily accounts; before, the last save on any website set it for all of them. A fallback language saved before this change is still shown for every website that has not saved its own.
- Storefront browse tracking under Magento's cookie notice counts only an acceptance on the current website, as Magento does: Magento keeps the websites a shopper accepted cookies on in one cookie, so on an installation whose websites share a cookie domain, accepting on one website no longer starts tracking on another until the shopper accepts there too (Luma and Hyvä). On Magento's standard theme, Magento's cookie notice is not shown again on a second website that shares the cookie domain, so such a shopper can accept there only through the store's own consent tool; on Hyvä the notice shows on each website. A cookie value that cannot be read is no consent, as in Magento's own cookie check.
Changes since 3.0.0-rc2
- Abandoned-cart reminders reach a guest who leaves Magento's checkout on the shipping step: Magento keeps a guest's email in the browser until the payment step, so such a cart had no email and was never reminded. While the abandoned-cart automation is on, the guest's email is now saved to the cart as soon as the checkout's email field holds a valid address (Luma-based themes and Hyvä's Luma-based checkout). One IP address (an IPv6 address with the rest of its /64) can save an email at most 30 times in 10 minutes, one cart takes at most five addresses, and the whole installation saves at most 2,000 an hour; behind a reverse proxy, Magento has to be set up to see the shopper's own IP address, as the user guide describes. While the automation is off, the checkout sends nothing. Who receives a reminder is unchanged: only a contact Smaily already has that has not unsubscribed.
- One email address gets at most one abandoned-cart reminder in 24 hours, whatever number of carts carry it: a further cart of that address is not mailed, and its row in the Log is Skipped with the reason (EN + ET).
- A data-subject erasure also stops a pending abandoned-cart reminder: every cart still open in the store that holds the erased address — on the cart or on its billing or shipping address — is marked erased, also one the extension had not picked up yet, so no reminder goes out for it. The carts themselves are not changed.
- A busy store keeps sending abandoned-cart reminders: once more than 100 carts of the last 24 hours had been reminded, skipped, ordered or erased, the abandoned-cart scan read only those carts and a new abandoned cart was never reminded. The scan now leaves out handled carts before it reads its 100 carts.
- A saved Smaily password is kept only for its own account: on Settings > Connection and in the initial setup, a save with a changed subdomain or username and an empty password is refused on the password field until the account's password is entered — also in a per-language account block, so a block that still shows a store view's old-language account cannot save the new account with the old password (EN + ET).
- With per-language Smaily accounts, saving Settings > Connection gives every store view the account of its current language, so a store view whose locale changed moves to the account of its new language. A store view whose language has no account uses the website's account, and an empty password takes the password saved for that account.
- The Campaign Intelligence catalog import and the nightly catalog re-sync send the products of every website, also one that does not sell on the default website (before, such a product reached Campaign Intelligence only when it was saved). A disabled, hidden or out-of-stock product goes as out of stock, so the nightly re-sync now corrects those too.
- The catalog import and the nightly catalog re-sync send each product with the same price and sale end date as a product save: a product on sale gets its sale end date, a sale that has ended or not started yet is not sent as a sale, and a fixed-price bundle gets its fixed price.
- The Campaign Intelligence engine wire contract is v1.8.2: one customer per visitor token, and a customer without a language leaves the Smaily contact's language as it is. What the extension sends is unchanged.
- Upgrading from 2.8.x: the upgrade notices in the admin inbox are translated (EN + ET), and the notice about the removed sync frequency says what 3.0 does — near-real-time sync with a 15-minute Smaily → Magento consent reconcile — instead of a daily full sync.
- The user guide has a new section, Third-party one-step checkouts, on Mageplaza One Step Checkout: where the opt-in checkbox shows, when a guest's email reaches the cart, how its own newsletter checkbox works with each sync mode, and three checks to run on a live store.
Changes since 3.0.0-rc1
- Stores that sell on a separate (headless) storefront: a new guide,
docs/HEADLESS_STOREFRONTS.md, says which features work with a separate storefront application, how product links are built and what the storefront team needs to do; the new Storefront URL setting puts the product links in the catalog sync and the RSS feed on the storefront's address. - Storefront browse tracking asks for marketing consent the way the WooCommerce plugin does: the store's own consent function first, then Magento's cookie notice, otherwise no consent. While no consent source is connected, Settings > Intelligence and an admin notification recommend one.
- The admin Log shows contact data in full for debugging, as the WooCommerce plugin's log does; passwords and API keys are never shown.
- The initial setup needs the settings permission, as Settings does.
- The server log file masks every email address, also a URL-encoded or JSON-escaped one and one quoted in an error that Smaily or Campaign Intelligence sends back.
- The Smaily API password and the Campaign Intelligence API key are marked sensitive, so
app:config:dumpdoes not write them toconfig.php. - Input hardening: the RSS feed is cached by the parameters it applies, and the RSS feed, the cart restore link, the browse tracking endpoint and the checkout opt-in read a request value that is not a single string as invalid input instead of answering with an error page.
- The release package is built from the committed source tree, and composer's dist installs leave out the same development files.
- The admin pages follow the Smaily Connect design across the initial setup, Dashboard, Settings and Log: step positions and titles, error banners that mark the field at fault, import cards with status pills, colored Log statuses, the Details panel with Send again and Copy payload, and Skipped for rows closed without being sent.
- Upgrading from 2.8.x: where Enable Module was No, contact sync and the welcome and abandoned-cart automations stay off; Smaily account values saved at store-view scope are not carried over, and an admin notice names those store views.
docs/UPGRADING.mdlists what to note down before the upgrade and what changes on upgrade day. - Switching abandoned-cart reminders on says to switch off any other tool that sends them, so a shopper does not get two reminders for one cart.
- This changelog lists each feature once.
Ground-up rewrite as module Smaily_Connect, targeting feature parity with the Smaily Connect plugins for WooCommerce and Shopify. Upgrading from 2.8.x is seamless: the composer package name is unchanged and all settings (including the previously plaintext API password, now encrypted) migrate automatically during setup:upgrade. Once migrated, the old 2.8.x settings (smaily/*, every scope) are deleted, so going back to 2.8.x starts with empty settings (see docs/UPGRADING.md).
New features
- Contact-sync lawful-basis modes: subscribers only (consent, default), all customers (legitimate interest), checkout opt-in only. Under checkout opt-in only, only the checkout newsletter checkbox creates or updates a Smaily contact: a signup through the newsletter form, the admin or the API alone sends nothing (with Magento's "Need to Confirm" on, a confirmed signup syncs, because the store cannot tell where it came from); an unsubscribe in the store still reaches Smaily. "Include guest order emails" applies only under all customers: under the other two modes a guest's email reaches Smaily only with the checkout opt-in. All customers is the EU soft opt-in: a customer who never unsubscribed is sent without a subscription status, so Smaily creates a new contact as subscribed and keeps an existing contact's status; a customer or guest who unsubscribed in the store is sent as unsubscribed on every send — a profile save, a guest order, the abandoned-cart purchase marker — so Smaily never creates them as a subscriber. The mode card in the initial setup and on Settings > Contacts says what soft opt-in requires — marketing only about similar products, a clear way to refuse at purchase, an unsubscribe link in every email — and that the merchant is responsible for the legal basis.
- Two-way consent sync: unsubscribes/resubscribes in Smaily mirror back onto Magento newsletter subscribers (action-log delta polling).
- Welcome and first-order automations alongside the abandoned cart automation; per-language workflow routing for multilingual stores (store view = language).
- Every store-event automation records its own last run on the Smaily contact —
welcome_automation_at,first_order_automation_at,abandoned_cart_automation_at(YYYY-MM-DD HH:MM:SSUTC, rewritten on every run) — so you can segment on "has received the welcome letter" or "got the abandoned-cart reminder more than 30 days ago". The same field names are used by the WooCommerce and Shopify plugins, so segments transfer between stores. - A purchase stops the abandoned-cart follow-ups: when a shopper the extension tracked as having abandoned a cart places an order, their Smaily contact gets
abandoned_cart_purchased_at(YYYY-MM-DD HH:MM:SSUTC, the same format and field name the WooCommerce plugin uses), so a workflow can exit whileabandoned_cart_purchased_atis later thanabandoned_cart_automation_at. A reminder that had not gone out yet is withdrawn instead of sent, and the field is written only for a shopper whose reminder went out to Smaily — so a withdrawn or failed reminder writes nothing — and only to a contact Smaily already has: the queue looks the address up in Smaily first, and for an address Smaily does not have the Log row is closed without being sent and says why. The field never creates a contact, and an ordinary purchase writes nothing. - The abandoned-cart reminder's
{{abandoned_cart_url}}restores the exact cart for 30 days after the reminder is created. After that the link opens the cart page with the notice "This cart link has expired." (EN + ET) and restores nothing. - Contacts can carry the customer's phone number — the default billing address telephone, synced as
user_phone(the same field name the WooCommerce plugin uses) when you tick Phone under Synchronized Fields. A customer without a number sends nothing at all, so a phone already in Smaily is never wiped. - Full multilingual admin UI: when store views speak more than one language, the Connection panel (initial setup step 1 / Settings > Connection) offers four routing-mode choice cards; "Per-language Smaily accounts" mode manages one credential block per language (side by side, each headed by its language, with its own Test connection and its own Connected / Not connected status) plus a default-fallback account picker, and both per-language modes add a per-language workflow mapping editor to the Automations panel with live per-account workflow lists and a default-fallback row per trigger. All sections adapt live to the selected mode — nothing saves until you say so. A mapped workflow always fires through the Smaily account its mapping row names (fallback rows included), matching the WooCommerce plugin's routing.
- Checkout newsletter opt-in checkbox (guests and customers, double opt-in respected).
- A native admin home under Marketing > Smaily Connect with four pages: Dashboard (one-sentence health verdict, connection status, truthful operational counters from the local queues — "Queued today" counts the events queued today that are still waiting to send, and the "delivered, last 30 days" tiles count only events that reached Smaily or Campaign Intelligence, never a skipped or withdrawn row — recent activity, where each row's status reads and is colored as in the Log, Skipped and Withdrawn included; failed deliveries in the last 24 hours add a warning banner above the verdict and a red Review failures button, a healthy verdict offers View full log), a guided five-step Initial setup (fresh installs land there automatically until setup is completed; each step is headed by its position, "Step 1 of 5", and its title; a save or a Test connection that fails there or on a Settings tab shows an error banner above the form and marks the field that caused it, with the message under it, until the field is edited; a finished setup reopens on a read-only summary of the connection marked Completed, with Edit credentials), a tabbed Settings page (Connection / Contacts / Automations / Intelligence / RSS — the initial setup's steps as always-available, deep-linkable tabs with instant per-tab AJAX saves — each tab has its own Save beside its own status, where a workflow list that cannot be loaded also says so — and live field reactivity) and one unified Log. These pages are the sole configuration surface — there is no separate Stores > Configuration entry.
- Stores that sell on a separate (headless) storefront: Settings > Connection > Using a separate storefront? > Storefront URL puts every product link in the catalog sync to Campaign Intelligence and in the RSS feed on the storefront's address — the scheme, host and port are replaced, the path and the query string stay; image links are unchanged, and empty keeps Magento's own links. Kept per website. Only an https address of a host alone is accepted (a path, a query or a fragment is refused on the field; a trailing slash is dropped), and a save that changes it asks for the catalog import to run again. The field opens by itself, saying why, when the orders of the last 30 days all came through Magento's API — GraphQL, or REST without Magento's storefront session — and none through Magento's own checkout (EN + ET).
- Multi-website support: installs with more than one Magento website get an explicit website selector on Settings (and a website-picker step in the initial setup), so each website keeps its own Smaily connection, contact sync and automation settings. Single-website installs are unaffected.
- Durable event queues with retries/backoff, one unified admin Log over both delivery queues (a Source column tells Smaily and Campaign Intelligence rows apart, and each row's status is a colored pill) with cross-queue mass retry, and health notices when deliveries keep failing. When one store action saves the same contact more than once, a save that changes nothing about the contact queues no second contact sync: a registration with the newsletter box ticked queues one under subscribers only.
- Per-row Details drill-down in the Log (a narrow slide-out panel headed by the event id, type and status): the attempts in order (queued, each attempt and its outcome, the next scheduled attempt — from what the row stores, so earlier attempts are listed without a time), the payload as sent (a row that went out in a batch shows only its own part), attempt count, next automatic retry time (or an honest "will not retry on its own"), last error and last API response (the HTTP status and what Smaily or Campaign Intelligence answered) — passwords and API keys are never shown; contact data is shown in full, as in the WooCommerce plugin's log, for debugging. Every delivery attempt on both queues records this evidence, and a retry never erases the evidence of the attempt before it; a row that never reached the server says that nothing was sent for it. A failed-deliveries banner above the grid and the dashboard's failed tile deep-link to the Log pre-filtered to failed rows. A failed row can be sent again from the Log: a new attempt is queued, the failed row is kept as the record of what went wrong, and the new row notes which row it repeats, who sent it and when. The Details panel offers the same Send again where the grid does, and Copy payload, which copies the payload as it shows it (EN + ET). Where sending again would reach the shopper twice or reach nobody — a reminder withdrawn because the shopper bought, a later message of the same kind already delivered (a later one that was skipped or withdrawn reached nobody and does not count), a contact erased under Art. 17 — there is no button, the row says why in one sentence, and the mass retry skips it and reports how many it skipped. A withdrawn reminder is labeled Withdrawn rather than sent or failed, and a row closed without being sent is labeled Skipped, with its reason in Details in the admin's language — the abandoned-cart purchase marker for an address Smaily does not have, an automation trigger with no Smaily workflow mapped, a personalization choice the shopper has changed since, or an identity link for a shopper who opted out of personalization (EN + ET).
- Historical import (backfill) of contacts, catalog, customers and orders — one click with live progress on the Settings page (Contacts / Intelligence tabs) or CLI. Each import is a card: a status pill in its header (Pending, Running, Done, Stopped, Canceled), a progress bar with "X of Y" and the percentage, Start import before the first run, only Cancel import while it runs, and Run again after it ends. A running import can be canceled (the worker stops cleanly at the next page boundary; starting again begins a fresh run), and the last outcome + timestamp persist on the panel with honest copy: "Done, X of Y synced", "… N failed" with a Log deep-link (items still pending retry are not counted as failed), "Stopped before an error", or "Canceled". The contact import obeys that website's contact-sync switch like every other outbound contact path: with the switch off the import control is disabled and explains why, and an import started any other way sends nothing. The contact import sends the contacts the website's contact sync mode covers — the newsletter subscribers under subscribers only, those plus every other registered customer under all customers, nobody under checkout opt-in only — a newsletter subscriber with its subscription status in the store (so under subscribers only nobody becomes subscribed by being imported), and under all customers every other customer without a status, as the live sync sends them; the estimate before the import counts the same contacts.
- After a major version upgrade a one-time admin notification suggests reviewing the settings or re-running the initial setup — settings are never changed or blocked.
- Campaign Intelligence integration: catalog/customer/order/browse ingest, recommendation attribution, identity merge, engine-run automations admin, GDPR export/erase CLI and a shopper personalization opt-out page. The My Account > Personalization page and its menu link appear only while Campaign Intelligence is connected and the account is active, as in the WooCommerce plugin; elsewhere the page is not found and a posted form changes nothing. The page shows the shopper's choice only when the store knows it (its own opt-out record or a successful read of the Smaily contact); when Smaily cannot be read it says the preference could not be loaded and still offers an opt-out button, instead of showing the default as the shopper's choice. Initial setup step 4 and Settings > Intelligence introduce it with the same text the WooCommerce and Shopify plugins carry: what it does with the store's product, customer and order data, and that it is an optional paid add-on (€250/month) added to the regular Smaily monthly payment, activated by contacting Smaily. Settings > Intelligence shows the introduction only until Campaign Intelligence is connected, as in the WooCommerce plugin; the setup step always shows it. Settings > Intelligence is headed by its title and a one-line description like the other tabs, and has its own Save only once Campaign Intelligence is connected (browse tracking is then the one thing to save); there is no page-wide Save.
- Storefront browse tracking asks for marketing consent the way the WooCommerce plugin does: first the store's own
window.smailyConnect.consentOverride()(for a consent tool such as Amasty or Cookiebot — examples in the User Guide, Connecting your cookie consent tool), then Magento's cookie notice when cookie restriction mode is on; with neither there is no consent. Without consent the tracker sends no browse event and sets no session cookie; campaign-click capture is unaffected. Consent given later on the same page starts tracking — on Magento's cookie notice event, or onsmaily:consent-changedfired by the store's consent script. When browse tracking is on and cookie restriction mode is off, the note under the browse-tracking toggle and an admin notification say that nothing is tracked until a consent source is connected and name the two ways: Magento's cookie restriction mode (linked to its settings) or the store's own consent tool (linked to the User Guide). - A shopper's personalization (profiling) choice reaches Campaign Intelligence reliably: it travels as a queued delivery with the normal retries, so an engine outage only delays it, and the store keeps its own record of the opt-out, which a failed Smaily write or a cache flush cannot undo. A delivery that is still waiting when the shopper changes their mind is not sent, so an older answer never overrides a newer one at the engine. The newest choice also wins against the Smaily contact: an older or undated "yes" there does not lift a newer opt-out made in the store, and the opt-out is written back to the contact. An opt-out recorded in Smaily reaches Campaign Intelligence the next time the store reads the shopper's preference (My Account or login). When a shopper who opted out logs in, their earlier anonymous browsing is no longer linked to their account. Unsubscribing from marketing — in the store, or in Smaily — now also stops profiling, as in the WooCommerce plugin; subscribing again turns profiling back on, and tells Campaign Intelligence, when the opt-out came only from unsubscribing; a profiling opt-out the shopper made on their own (My Account > Personalization or in Smaily) stays. Recording a choice never creates a Smaily contact or subscribes anyone: the choice is written only to a contact Smaily already has. The store's opt-out record holds no address: each entry is keyed by an HMAC of the address with the store's crypt key; an entry kept under the earlier plain hash, or under a crypt key since rotated, is still read and is moved to the current key on its next change. The cached preference answers are keyed the same way.
- A data-subject erasure now reaches the store's own tables, not just Campaign Intelligence:
bin/magento smaily:gdpr erase <email> --forcedeletes every queued message that could still be sent to that contact and anonymizes the ones already sent (they stay in the Log as your record, showing[erased]in place of the address, the payload and the response), anonymizes the contact's abandoned-cart record and keeps it (the address goes, the record stays so the cart is never picked up and mailed again), and prints a count per table. The local part runs first, so an engine outage never blocks it.smaily:gdpr exportlists the same rows. - A deactivated Campaign Intelligence account is remembered and acted on instead of retried: every send path (ingest flush, engine-bound historical imports, the browse relay) stops, queued rows are left untouched and go out in order once the account is active again, and Settings > Intelligence, the Dashboard verdict and the admin notification say plainly that the account is not active on the Smaily side — with a Check again button that resumes everything the moment it is. Smaily email sending and the contact import are unaffected.
- The Smaily connection status tells the truth: the Dashboard and Settings > Connection say Connected only when Smaily accepted the saved credentials at the last check — Test connection, or any save of the connection, which now asks Smaily once without ever blocking the save. Changed credentials stay Not connected until they are checked, and a later refusal by Smaily (HTTP 401/403, also on a queued delivery) turns the status back to Not connected. The Dashboard has a Not connected verdict with an Open Connection settings button; it outranks failure counts, which a refused connection explains. No page load calls Smaily, and the credentials are remembered only as keyed hashes. When the Smaily package does not include API access (Smaily code 227), Test connection, the connection status, a configuration save, the Dashboard and the Log say exactly that instead of calling the credentials refused — as in the WooCommerce plugin. The store shows Not connected, because nothing gets through, and the credentials are checked again once the package includes the API. Test connection with a blank password uses the saved one when the subdomain and username are filled in; with empty fields and nothing saved it asks for the subdomain, username and password in plain words (EN + ET). The initial setup's Overview step says Smaily Connect is syncing only when it is connected; otherwise it says syncing starts once Smaily accepts the credentials and points to Settings > Connection. The Settings > Connection status line is part of the page as it loads, so it never shows Not connected for a moment before the real status, and it shows the answer of Test connection and of a save at once, without a reload; a save with credentials Smaily does not accept shows Saved. and Not connected.
- RSS feed improvements: category ID filter, limit/sort/order parameters, cache headers, enable/disable toggle [#48, #49, #50, #72]
- Feed URL Builder on the Settings > RSS tab: pick category/limit/sorting and copy the ready feed URL with one click; the initial setup's Overview step links to it.
- The initial setup's step rail shows a short description under each step name, worded as in the WooCommerce plugin's setup (English and Estonian).
- In-admin documentation: the initial setup's Overview step and the post-install notice link to the full user guide. Finishing the initial setup marks the post-install notice ("Smaily Connect is ready to set up") as read. The notice says that settings from an earlier version were migrated only when an upgrade from 2.8.x migrated them.
- A dedicated log file,
var/log/smaily_connect.log, that records errors only by default [#113]. There is no admin control for the log level: a developer raises it for troubleshooting withbin/magento config:set smaily_connect/logging/verbosity info|debugand sets it back toerrorafterwards, because the detailed levels write more about contacts to a file on the server (see the user guide). - Translations: full English (
i18n/en_US.csv) and Estonian (i18n/et_EE.csv) translation packs covering the admin (initial setup, configuration, grids) and the storefront (checkout opt-in, personalization page). API and engine error messages surfaced in the admin are translated too, framed in sentences that stay understandable even when the remote service's own message is technical. A Smaily delivery error is stored in English and shown in the Log (grid and Details) in the admin's language, whatever the language of the store that sent the row; the log file records it in English.
Under the hood
- New module name
Smaily_Connect(namespaceSmaily\Connect); composer package name unchanged. - Declared PHP (8.1–8.4) and Magento (2.4.4+) requirements in composer.json [#18]
- No more columns on the core
quotetable; legacyreminder_date/is_sentcolumns and the unusedsmaily_customer_synctable are cleaned up on upgrade. - Store-timezone-safe scheduling (the hardcoded Europe/Tallinn timezone is gone); Guzzle-based API clients with timeouts and typed errors.
- Legacy custom captcha replaced by Magento's native reCAPTCHA module (admin notice on upgrade).
- Unit tests, phpcs/phpstan static analysis and CI added [#51]
- Integration test suite against a real MySQL (queue retry/backoff/claim semantics, the 2.8.x → v3 settings and schema migration, queue cron flows with stubbed HTTP transports), run in CI with a MySQL 8.4 service — see TESTING.md.
- Ships the Campaign Intelligence engine wire contract (
docs/RECENGINE_API_CONTRACT.md, v1.12.0, byte-synced across Smaily connect repositories): order amounts gross/tax-inclusive (row_total_incl_tax/grand_total), and browse events taggedsource: plugin_magento. - Stock changes reach Campaign Intelligence, not just product edits: a shipment that sells the last unit, a credit memo that returns it, an Advanced Inventory or Sources edit and the stock REST endpoints all re-sync the product, on both Multi-Source Inventory and legacy-inventory installs. A change no event can see, such as a CSV import writing the catalog tables directly, reaches Campaign Intelligence with the product's next save or stock change, or with a catalog import started by hand (there is no scheduled full re-sync since 3.0.0-rc6); otherwise a product's availability is a minute or two stale.
- Catalog rows carry the platform parent product id as
tags.product_id(a configurable child resolves to its parent's entity id). A product hard-delete soft-removes the whole product engine-side viaPOST /api/v1/ingest/catalog/remove(contract §3b); a configurable child's deletion keeps the per-SKU out-of-stock path, and disabling a product remains a soft out-of-stock update. - A product with no real category (none assigned, or only the store's root category) is sent to Campaign Intelligence with the placeholder category
uncategorizedandtags.category_defaulted: "true"(engine contract §3, v1.6.0), as the WooCommerce and Shopify plugins do. The engine then derives nothing from the placeholder and classifies the product by its name instead. A product in a real category carries no flag. - Automation workflow dropdowns list only workflows the Smaily API can actually trigger (
GET workflows.php?trigger_type=form_submitted, the same listing the WooCommerce plugin uses). Listing every ACTIVE automation instead (GET autoresponder.php) offered workflows whose trigger type is not "form submitted" — selecting one made every automation fail with Smaily's misleading error 221 "Invalid autoresponder ID". Disabled workflows are excluded too: enrolling one returns OK but silently sends nothing. Verified against a live Smaily account. - Contact and abandoned-cart payloads now carry the real store group name in
store_group(a wrong accessor left it always empty). - Saving the Campaign Intelligence automations no longer silently drops a workflow binding whose Smaily workflow has since been deleted (or when the workflow list fails to load): the missing workflow is kept and shown as "Workflow #N (not in your Smaily list — kept)" rather than reset to "Not Selected". Deliberately clearing a workflow that is still in the list works as before.
- Background jobs (event queue flush, ingest queue flush, backfill tick, abandoned cart, contact reconcile, health check, queue janitor) now actually run: the module's custom
smaily_connectcron group was missing itsetc/cron_groups.xmldefinition, so Magento's cron scheduler never picked up a cadence for it and none of the 7 jobs was ever scheduled, on any install. Fixed by adding the missing definition file. - The abandoned-cart job now completes: the guest-cart widening had left an ambiguous column reference in its quote query, so MySQL rejected the whole SELECT and the job failed on every run — no reminder was ever sent, on any install. Fixed by qualifying the filter columns.
- A delivery Smaily refuses outright — revoked credentials, a deleted workflow, a rejected address — is now marked failed on the first refusal, with the refusal in the last error, instead of being re-sent five times over about 81 minutes: the Log, the failed-deliveries banner and the dashboard tile report it straight away. When Smaily asks the store to slow down (HTTP 429) the delivery waits exactly as long as Smaily asked. Server errors and network failures keep the existing ladder (the next attempt 1 min, 5 min, 15 min, then 1 h later; five attempts in all). Same classification the sibling Smaily plugins use.
- A network failure on a call to Smaily or Campaign Intelligence no longer writes a contact's email address anywhere. The failure text used to end with the full request address — for the consent lookup that included the email in its query string, for the Campaign Intelligence customer calls the email in the path — and that text reached
var/log/smaily_connect.log(even at the default errors-only level), a queued delivery's last error and the Log. The text now keeps the server and path, drops the query string, and shows{email}where the address was in the path. - A malformed recommendation id no longer costs Campaign Intelligence the order. The engine refuses a whole order over a
smaily_rec_idthat is not a well-formed UUID; the storefront now stores the id from a campaign link (smaily_rec, or theutm_contentfallback) only when it is well-formed, and the order sent to the engine leaves out a malformed stored id, so the order arrives without that attribution instead of not at all. - An over-long or malformed attribution cookie no longer costs an order its other attribution. The visitor token, context and session id are checked against the shapes the WooCommerce plugin uses, each up to 64 characters, when the storefront stores them, when they are stamped onto the order and when the order is sent. One that does not fit is left out on its own. Before, a database running a strict SQL mode refused the order's whole attribution record over one over-long value, and Magento's default mode stored it cut short.
- The storefront browse relay is hardened: it rate-limits by the connection's own address (Magento's remote address, so a forwarding header counts only where the store's configuration names it), it forwards each batch in one short attempt (3 seconds, no retry, no wait) so it never holds a storefront request on Campaign Intelligence, and it forwards no customer identifier from the browser — identity on browse events comes only from the visitor token and the login identity merge. Every Campaign Intelligence call now also has a connect timeout, and a back-off the engine asks for is honored up to 60 seconds.
- The Campaign Intelligence setup address is validated: the setup URL must be an https address on
intelligence.smaily.com(a bare token is exchanged there), and the connection is saved only when the engine base URL and every endpoint the engine answers with are https addresses onintelligence.smaily.comtoo. Anything else is refused with a message in the admin, and nothing is sent. A connection saved earlier keeps working as it is. - The Smaily subdomain must be a plain subdomain such as
demo— letters, digits and hyphens. Pasting the fullhttps://demo.sendsmaily.netaddress still works and is reduced todemo. Any other value is refused with a message on save (Settings, Initial setup, per-language accounts,config:set) and on Test connection and the workflow list, and no request is sent with it — neither the typed password nor the stored one. - Contract staleness guard in CI: a dedicated daily "Contract staleness" workflow (
bin/check-contract-staleness.sh) fails when the vendoreddocs/RECENGINE_API_CONTRACT.mdis no longer byte-identical with the engine repo's main branch. - Hyvä theme compatibility module (
compat/hyva/, moduleHyva_SmailyConnect, to be published as the separate packagesmaily/module-connect-hyva): framework-free browse tracker and attribution scripts (no RequireJS/jQuery, no inline executable script — nothing to whitelist even under strict CSP), a Tailwind-styled personalization page and Tailwind-build registration viahyva:config:generate. Verified end-to-end on Hyvä 1.5.2 — default theme and the strict-CSP variant (Hyva/default-csp, storefront CSP enforced withoutunsafe-inline), with a Luma store view as the regression control; the free Hyvä tier's Luma-fallback checkout keeps the checkout opt-in working unchanged. Audit and the executed verification matrix in docs/HYVA_SUPPORT.md; excluded from the release ZIP. - The marketing event queue carries an index on
(entity_id, event_type, status): the lookup that withdraws a shopper's still-pending abandoned-cart reminder runs on every order placed, and on a busy store's queue it was a full table scan. - The release ZIP is assembled by one script (
bin/build-release-zip.sh) that both the release workflow and the new packaging check call, and every push now builds the package and verifies it (bin/verify-release-zip.sh): required module files present, development material and the separately published Hyvä companion absent, the archived version equal to the repository's,php -lclean on every shipped file, and a SHA-256 build hash written beside the artifact. Published releases attach thatsmaily-connect-magento2.zip.sha256file next to the ZIP, so a manual installer can verify the archive withsha256sum -cbefore extracting it. - Package manifest completeness:
magento/module-catalog-inventoryandmagento/module-uiare used by the module but were never declared in composer.json — an install that had them removed would have failed at runtime rather than at composer time. - Module manifest completeness:
etc/module.xmlnow sequencesMagento_CatalogInventoryandMagento_Uialongside the other required Magento modules, so Magento loads the module after everything it depends on. The optional Multi-Source Inventory packages the Campaign Intelligence stock hooks plug into are listed under composersuggest— they are wired throughetc/di.xmlonly, and an install without MSI works unchanged. - Uninstalling the module removes its settings from the store database, as the WooCommerce plugin does: every
smaily_connect/setting at every scope (the encrypted Smaily API password and the Campaign Intelligence API key included), the settings kept from 2.8.x (the old plain-text password included) and the module's flag rows (the personalization opt-out record, the checked-credentials record, health and consent-sync state) and the "Smaily Connect is ready to set up" admin notice. It runs onbin/magento module:uninstall --remove-data Smaily_Connectfor a composer install and onbin/magento module:uninstall --non-composer Smaily_Connectfor a ZIP install inapp/code. Disabling the module keeps everything. Seedocs/INSTALLING.md. - Hardening: the public RSS feed is cached by its parameters as the feed applies them — an invalid or out-of-range limit, sort or order shares the cache entry of the value it falls back to, so varying it cannot force a rebuild. The storefront endpoints (the RSS feed, the browse relay, the checkout opt-in and the abandoned-cart restore link) treat a request value that is not a single string — a repeated query key, a JSON array or object — as invalid input instead of answering with an error page. The Smaily API password and the Campaign Intelligence API key are declared sensitive, so
bin/magento app:config:dumpleaves them out ofapp/etc/config.php. The release ZIP is built withgit archivefrom the committed tree, so a file that is not committed (a local.env, an IDE folder) cannot ship, and the packaging check refuses any dot-file; the files left out are listed once asexport-ignorein.gitattributes, so a composer dist install from GitHub leaves out the same development material. composer.json now requiresguzzlehttp/guzzle(^7.4), which the API clients use directly. The GitHub Actions workflows pin every action to a full commit SHA, with its version beside it. Every email address the extension writes tovar/log/smaily_connect.logis masked to its first characters (j***@e***.com), at every log level — also one written URL-encoded (%40) or JSON-escaped (\u0040), and one quoted in an error that Smaily or Campaign Intelligence sends back. - The initial setup needs the Smaily Connect settings permission, as Settings does, so an admin role without it no longer sees the saved subdomain and API username there.
Behavior changes
- Upgrading from 2.8.x: where Enable Module was No (Default Config or a website), contact sync, the welcome and the abandoned-cart automation are switched off at that scope, while a website with its own Yes under a Default Config with No keeps the values it ran with in 2.8.x; a Smaily account value saved at store-view scope (which 2.8.x never used) is not carried over, and an admin notice names those store views. On upgrade day the checkout newsletter checkbox is on, Magento's own subscription-confirmed and unsubscribe emails are suppressed, and contacts carry
first_name/last_nameinstead ofname(seedocs/UPGRADING.md). - Contact sync frequency presets are gone: v3 syncs in near-real-time via observers + a 15-minute consent reconcile.
- Gender now reaches Smaily under the field name
user_gender(2.8.x sentgender), the name Smaily's WooCommerce plugin uses, so one shopper syncing from two stores lands in one field. Your tick in Synchronized Fields migrates automatically; Smaily segments and templates that referencegenderneed repointing touser_genderonce. - The RSS feed lists catalog-visible products only; configurable variants resolve to their parent.
- An automation never re-subscribes a contact who unsubscribed in Smaily, in any contact sync mode. The release candidate's Automations May Re-Subscribe (Advanced) setting (legitimate interest mode only) is gone, the same choice the WooCommerce plugin retired. The update (
setup:upgrade) deletes its stored value at every scope, as the WooCommerce plugin did in 3.11.1, and touches no other setting. On a store that had it on, the welcome, first-order and abandoned-cart triggers now honor every unsubscribe, and contacts keep syncing with their real subscription state. An automation reaches only a contact Smaily already has: for an address Smaily does not have, the trigger creates no contact and sends nothing. - The welcome automation fires only when a shopper subscribes in your store — the newsletter form, registration, their account's newsletter page, the checkout opt-in or a double opt-in confirmation — and again when they resubscribe there. A subscription made in the admin, through the REST, SOAP or GraphQL API or by an import still syncs the contact, but sends no welcome. The WooCommerce plugin applies the same rule to accounts the shopper did not create.
2.8.1
Fixes an issue with cron scheduling using wrong interval for daily customer synchronization.
2.8.0
Notice! This version updates the price values in abandoned cart emails and RSS feed items to include taxes. These prices now match what customers see in the storefront. For B2B (business-to-business) stores, where tax-exclusive pricing may be expected, this behavior might not be suitable.
- Abandoned cart
product_priceandproduct_base_pricenow also include taxes. - RSS-feed now shows prices including taxes.
- RSS-feed uses parent product URL-s for configurable products that are not visible individually.
2.7.7
- Adds
"is_abandoned_cart" = "true"field to abandoned cart automation payload - Does not opt-in unsubscribed customers who have received abandoned cart email
2.7.6
- fix: RSS feed rendering with missing description value [#118]
2.7.5
- fix: Items placement in RSS feed structure [#114]
2.7.4
- Fixes an issue where abandoned cart synchronization can fail when unknown payload field is encountered.[#111]
2.7.3
- Fixes non-existing array key warning on subscribers synchronization [#108] (thanks @raulikesvatera)
2.7.2
- PHP 8.2 compatibility [#103]
2.7.1
- Skip abandoned carts receiving "Invalid data submitted" (code: 203) response - [#99]
2.7.0
- Add store, store group and website to abandoned cart payload - [#96]
2.6.0
- Compare subscriber status change timestamp on newsletter subscriber sync [#91]
- Fix newsletter subscribers sync unsubscribed status value [#92]
2.5.0
- Add product image URL to abandoned cart data payload [#88]
2.4.0
- Include more context in CRON job logs [#82]
- Fix CRON job logging duplicate lines [#83]
- Optimize abandoned cart CRON job by excluding sent carts [#84]
2.3.1
- Test for Magento 2.4.4 compatibility - [#78]
- Convert module schema and data setup to declarative schema - [#77]
2.3.0
- Newsletter Subscribers synchronization tracking per website - [#73]
- Make last synchronization datetime configurable in module settings - [#73]
2.2.0
- Include store group and website in opt-in form and synchronized data [#67]
- Add automation workflow selection to Newsletter Subscriber settings [#68]
2.1.0
- Magento 2.4 compatibility [#63]
2.0.0
This is a complete rework of the module. The aim was to make the module configurable by website, i.e. abandoned cart, newsletter subscribers synchronization, opt-in form and Smaily API could be configured for each website. Only reasonable solution was to rebuild the module from ground up, because most (if not all) of the functionality was "Default configuration"-centric.
- Improves efficiency of Newsletter Subscribers and Abandoned Cart CRON jobs [#36]
- Reduces bloatiness of data Helper [#37]
- Fixes double CAPTCHA input fields [#46]
1.2.0
- Align synchronization customer first and last name with abandoned cart [#52]
1.1.0
- Add new fields
first_nameandlast_namefor abandoned cart export - Changes
product_qtyfield toproduct_quantityto unify template variables across integrations
1.0.2
- Fix RSS-feed not rendering with special characters
1.0.1
- Fix PHP 5.6 compilation issues
1.0.0
- Make using CAPTCHA optional for better integration with pop-up forms
0.9.3
- Add Magento CAPTCHA and Google reCAPTCHA option for newsletter sign-up form
0.9.2
- Fix compilation issues
0.9.1
- Subdomain is now parsed from full URL
- Newsletter signup form uses opt-in autoresponder workflow
- Updated cron frequency values
- Updated abandoned cart timing values
- Customer synchronization is now more efficient as it uses data batching
- Customer unsubscribed status is also updated in store's database
- Uninstall cleans up created tables and columns
- Removed custom newsletter and email template blocks
- Removed subscriber observer as synchronization provides same functionality
- Fixed broken links in settings from
0.9.0
- This is the first public release
| Version | Stability | QA Status | Compatibility | Released |
|---|---|---|---|---|
| 3.0.0-rc11 | RC | Not tested | Not yet tested Details | 2026-10-09 21:42:09 |
| 3.0.0-rc10 | RC | Not tested | Not yet tested Details | 2026-10-07 19:57:10 |
| 3.0.0-rc9 | RC | Not tested | Not yet tested Details | 2026-10-07 15:03:31 |
| 2.8.1 | stable | Fail | Not yet tested Details | 2026-04-29 09:04:42 |
| 2.8.0 | stable | Not tested | Not yet tested Details | 2025-04-11 09:04:15 |
| 2.7.7 | stable | Not tested | Not yet tested Details | 2025-04-03 08:46:22 |
| 2.7.6 | stable | Not tested | Not yet tested Details | 2025-03-20 15:19:26 |
| 2.7.5 | stable | Not tested | Not yet tested Details | 2025-03-17 13:48:23 |
| 2.7.4 | stable | Not tested | Not yet tested Details | 2024-10-16 10:19:27 |
| 2.7.3 | stable | Not tested | Not yet tested Details | 2024-05-06 15:47:55 |
| 2.7.2 | stable | Not tested | Not yet tested Details | 2023-12-11 09:36:24 |
| 2.7.1 | stable | Not tested | Not yet tested Details | 2023-11-03 13:29:09 |
| 2.7.0 | stable | Not tested | Not yet tested Details | 2023-01-06 09:27:58 |
| 2.6.0 | stable | Not tested | Not yet tested Details | 2022-12-12 08:42:43 |
| 2.5.0 | stable | Not tested | Not yet tested Details | 2022-12-08 12:56:54 |
| 2.4.0 | stable | Not tested | Not yet tested Details | 2022-11-10 10:59:42 |
| 2.3.1 | stable | Not tested | Not yet tested Details | 2022-06-20 13:46:08 |
| 2.3.0 | stable | Not tested | Not yet tested Details | 2022-03-18 14:56:10 |
| 2.2.0 | stable | Not tested | Not yet tested Details | 2021-10-07 11:42:53 |
| 2.1.0 | stable | Not tested | Not yet tested Details | 2021-08-06 12:27:38 |
| 2.0.0 | stable | Not tested | Not yet tested Details | 2021-07-02 07:28:33 |
| 1.2.0 | stable | Not tested | Not yet tested Details | 2021-03-06 12:15:26 |
| 1.0.2 | stable | Not tested | Not yet tested Details | 2019-11-19 10:32:27 |
| 1.0.1 | stable | Not tested | Not yet tested Details | 2019-11-07 13:31:08 |
| 1.0.0 | stable | Not tested | Not yet tested Details | 2019-11-07 11:47:21 |
| 0.9.3 | stable | Not tested | Not yet tested Details | 2019-10-14 06:35:14 |
| 0.9.2 | stable | Not tested | Not yet tested Details | 2019-10-11 10:09:17 |
| 0.9.1 | stable | Not tested | Not yet tested Details | 2019-09-12 08:44:48 |
| 0.9.0 | stable | Not tested | Not yet tested Details | 2019-01-21 11:21:53 |
Suggests 2
| Package | Reason |
|---|---|
| magento/module-inventory-api | Campaign Intelligence re-syncs a product when a Multi-Source Inventory source item is saved (plugin on SourceItemsSaveInterface); without MSI the legacy stock observer covers it. |
| magento/module-inventory-source-deduction-api | Campaign Intelligence re-syncs a product when a shipment deducts stock or a credit memo returns it under MSI (plugin on SourceDeductionServiceInterface); without MSI the legacy stock observer covers it. |
No QA results yet
QA pipelines haven't run for this version. Compatibility and quality results appear here once the vendor publishes a tagged release that gets ingested.
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.