Version 1.0.0

August 28, 2026
  • Added historical order sync: php craft commerce-klaviyo/sync-orders --from=YYYY-MM-DD --to=YYYY-MM-DD and a Utilities → Klaviyo historical orders screen. Queues Placed Order / Ordered Product for completed orders in the dateOrdered range, stamped with the original order time.
  • Added Klaviyo List and Klaviyo Lists field types for entries/users so Twig forms can read entry.myList.id / loop entry.myLists when posting to track/subscribe actions.
  • Added optional public track/identify actions (publicTrackActionsEnabled, off by default): commerce-klaviyo/api/identify and commerce-klaviyo/api/track queue profile upserts, custom events, DOI-aware list subscribe, and optional Commerce order tracking from Twig forms.
  • Added Updated Cart tracking for abandoned-cart flows: incomplete carts with an email fire Klaviyo’s reserved metric when line items or the order total change (address-only saves are ignored). New setting trackUpdatedCart (default on) on the Order tracking tab.
  • Added public commerce-klaviyo/cart/restore?number=… action that redirects to Commerce’s load-cart URL for incomplete carts (404 when missing or completed). Started Checkout / Updated Cart payloads include OrderId (cart number) alongside CheckoutURL for mail templates.
  • Fixed bulk catalog reindex sending a double-wrapped JSON:API payload that Klaviyo rejected on every job. Each bulk entry's payload was already a full single-create {data: {type, attributes}} envelope from CatalogPayloadBuilder, then nested again into items.data[] / variants.data[]. Klaviyo responded with "'type' is a required field for the resource 'request-data'" because type sat one level too deep. The same wrap bug existed on the bulk update path. Once unwrapped, real reindexes still timed out: Klaviyo bulk jobs for a handful of items routinely take several minutes, past the old 180s poll budget and Craft's 300s default ttr. Polling is now 200 × 3s (600s) and bulk chunk jobs use a 1500s ttr.
  • Package C — catalog API polish: Klaviyo HTTP client now sends Accept / Content-Type: application/vnd.api+json (JSON:API media type); catalog item/variant payloads include image_thumbnail_url and the full images gallery from the configured Assets field; variant sync prefers a variant-level image field over the product (same fallback order as title/metadata). No automatic storefront back-in-stock form injection — themes still use the settings snippet.
  • Package B — back-in-stock hardening: the public back-in-stock action now rejects signups when the variant does not track inventory or is currently in stock (server-side stock guard, independent of theme markup); Klaviyo API errors are mapped to clearer customer messages (duplicate subscription, missing catalog variant, generic retry); optional also subscribe to a Klaviyo list setting on the Back in stock tab queues a list subscription after a successful signup (same queued API as newsletter signup).
  • Package A — catalog reliability: variant sync jobs now upsert the parent catalog item first, so parallel queue workers no longer hit Klaviyo with orphaned variants; category sync diffs linked categories and unlinks ones removed in Craft; full reindex uses Klaviyo's bulk catalog create/update jobs (100 per call) instead of one queue job per element; the Catalog sync settings tab adds catalog health (last success, last reindex, last bulk job) plus a Klaviyo lookup by Craft element ID (catalogs:read scope).
  • Added a Back in stock settings tab with a copyable Twig form snippet for product templates (POST to commerce-klaviyo/subscriptions/back-in-stock) and an optional inventory reporting threshold: tracked variants only send real stock to Klaviyo once quantity is at or below N; above N they report a high placeholder so low-inventory / back-in-stock flows ignore high-stock noise. No automatic storefront injection — themes still paste the snippet.
  • Fixed a critical bug where catalog variant sync sent no data to Klaviyo, ever. CatalogSyncService::syncVariant() built a correct payload and then queued SyncCatalogVariantJob with an empty constructor array, discarding variantExternalId, productTitle, and payload entirely — every queued job ran with all three at their empty defaults. Product-level sync (SyncCatalogItemJob) was unaffected; only variants, which is where price, SKU, and inventory actually live. Found live while seeding real product data to screenshot the plugin for documentation, confirmed by inspecting the actual queued job before and after.
  • Fixed a crash when the configured image or description field exists only on the product, not the variant — exactly the setup this plugin's own settings screen describes ("field on products", checked on the variant first). CatalogFieldResolver called Element::getFieldValue() without checking whether the handle was even on that element's field layout first; Craft throws for an unknown handle rather than returning null. This crashed onsite tracking's Viewed Product (the whole page render) and Added to Cart (the whole cart request) outright, and would have crashed catalog sync too had CatalogSyncService ever tried the variant first for these fields (it doesn't, which is why that path looked fine until onsite tracking was actually exercised).
  • Fixed the "Test connection" and "Refresh lists" buttons doing nothing when clicked from the real Settings → Plugins page. Both looked up their DOM elements by literal id; Craft's generic plugin-settings page renders every field through view->namespaceInputs(), which rewrites every id to settings-<original> — the literal lookups always returned null, so neither click handler was ever attached.
  • Fixed the entire settings screen crashing with a 500 whenever a newsletter list ID was configured and the API key was invalid or Klaviyo was unreachable. settingsHtml() calls KlaviyoListsService::getOptInProcessForList() unconditionally on every page load (not just an explicit "Refresh lists" click), and nothing upstream caught a Klaviyo API failure — a merchant whose key expired after configuring a list would be locked out of the settings screen entirely, with no way to open it and fix the key. The "Refresh lists" button's own, already-correct error reporting is preserved via a separate non-throwing method, so it still shows the real Klaviyo error instead of silently reporting success with zero lists.
  • Fixed any console-context order save crashing outright (an import script, a migration, a queue job run via php craft queue/exec, another plugin's seed data) whenever the order wasn't yet complete. OrderTrackingService::trackStartedCheckout() runs unconditionally on Order::EVENT_AFTER_SAVE and calls resolveCheckoutUrl(), which called Commerce's own Order::getLoadCartUrl() — that method (via Carts::getLoadCartUrl()) calls $request->setIsCpRequest(false), a method that only exists on craft\web\Request, not craft\console\Request. Any console request saving a non-completed order threw an uncaught UnknownMethodException out of the save itself, not just out of the tracking call. Found live while seeding demo data for another plugin's documentation and noticing order creation from a console script fail. Fixed by treating a failure to build the URL as "no checkout URL available" (the event still queues, just without CheckoutURL) instead of letting it propagate.

All four found live while seeding real product, category, image, and (intentionally invalid) Klaviyo credential data to screenshot the plugin for documentation.

  • Standard ecommerce event tracking using Klaviyo's own reserved metric names: Started Checkout, Placed Order, Ordered Product, Fulfilled Order, Cancelled Order, Refunded Order.
  • Real-time Klaviyo Catalog API sync for products and variants, including inventory quantity — powers Klaviyo's own back-in-stock detection.
  • A public, server-side back-in-stock subscription endpoint (email channel only).
  • Custom profile field mapping — map any Craft user field to any Klaviyo profile property.
  • Every Klaviyo API call runs on the queue with its own error handling; a Klaviyo outage never blocks checkout.
  • New queueComponentId setting: route this plugin's sync jobs to a custom Yii application component (e.g. a real message-queue backend configured in config/app.php) instead of Craft's default queue, isolating Klaviyo traffic from the rest of the site's queue jobs. Falls back to the default queue, logged rather than thrown, if the configured component isn't available.
  • New optional newsletter signup (newsletterSignupEnabled, off by default): a public, server-side form action to subscribe an email to a Klaviyo list, plus optional support for binding an existing Formie form as the newsletter signup form instead — no separate Formie-side Klaviyo integration/API key needed. Has no effect and no dependency on Formie unless both the setting is on and Formie is actually installed.
  • Fixed: catalog variants were never actually reaching Klaviyo on a real product save. The sync was wired to the product's EVENT_AFTER_SAVE, at which point Commerce hasn't persisted the product's variants yet — now synced on each variant's own EVENT_AFTER_SAVE instead, where its id is reliably available.
  • Added explicit request timeouts (5s connect, 15s total) to the Klaviyo API client. Craft's shared Guzzle client sets none, so an unresponsive Klaviyo previously held requests open until PHP's execution limit — most visibly on the public, anonymously accessible back-in-stock endpoint, which calls Klaviyo synchronously.
  • Fixed: drafts and revisions were being synced to the Klaviyo catalog as if they were real products. Because Craft creates a revision on every control-panel publish, an actively-edited store accumulated a junk catalog item and variant per edit — keyed to the draft/revision's element ID, never removed, and still eligible for product blocks and recommendations. Drafts, revisions, and multi-site propagation saves are now all skipped.
  • Fixed: removing a single variant from a product left its catalog variant in Klaviyo forever. Variant removals now issue a catalog-variant delete.
  • Fixed: the custom profile field mapping setting was never actually applied — order-tracking events (Started Checkout, Placed Order, Fulfilled/Cancelled/Refunded Order) sent only the customer's email, and the newsletter signup's Formie bridge ignored the mapping entirely. Both now apply it: order events read from the order's associated Craft User, the Formie bridge from the submitted form's fields.
  • Fixed (KL-03): the API key setting only ever accepted a plain value — no App::parseEnv() call anywhere in the code path, so typing $KLAVIYO_API_KEY (which the field looked like it should accept) would have been sent to Klaviyo as that literal string. Settings::getApiKey() now resolves it properly; the settings field is an env-var-aware autosuggest.
  • Added (KL-04): connection test — a Test connection button on the settings screen and php craft commerce-klaviyo/test, both a single lightweight GET /api/accounts/ call with no side effects on the Klaviyo account.
  • Added (KL-01): php craft commerce-klaviyo/reindex — re-queues every published product and its variants for a full catalog resync, reusing the exact same CatalogSyncService::syncProduct()/syncVariant() methods a real-time save uses. Each product's job is enqueued before its variants' jobs. Mutex-guarded against concurrent runs.
  • Added (KL-05): configurable descriptionFieldHandle and imageFieldHandle settings — map project-specific custom fields to Klaviyo's catalog description and image_full_url. Description still falls back to the product title; the image is still omitted when no handle is configured.
  • Added (KL-06): Started Checkout events now include a CheckoutURL property, set from Commerce's own Order::getLoadCartUrl() so abandoned-cart flows can link back to the cart.
  • Added (KL-07): profile field mapping now also reads from the order's billing address (shipping as fallback) on guest checkout, not just from the associated Craft user.
  • Added (KL-08): the settings screen shows the most recent catalog-sync and event-track failures with a timestamp, recorded when a queue job fails.
  • Added (KL-10): EVENT_BEFORE_BUILD_*_PAYLOAD hooks on CommerceKlaviyo so agencies can adjust Klaviyo request bodies before they are queued or sent (catalog, track events, newsletter, back-in-stock).
  • Added (KL-11): newsletter settings now load Klaviyo lists from the API (with opt-in process shown) instead of requiring a manually pasted list ID.
  • Added (KL-09): optional inbound Klaviyo system-webhook endpoint (commerce-klaviyo/webhook/receive) that verifies HMAC signatures and writes unsubscribe/resubscribe state to a configured Craft user field.
  • Added (KL-12): optional onsite tracking (onsiteTrackingEnabled, off by default) — loads Klaviyo's public JavaScript on the storefront, tracks Viewed Product on product templates, and Added to Cart on Commerce cart updates (AJAX JSON payload + session fallback for classic form posts). Variant IDs match catalog sync and order events.