How to Build a Custom 'Abandoned Cart' Recovery Strategy Beyond Emails (SMS, Push)

How to Build a Custom 'Abandoned Cart' Recovery Strategy Beyond Emails (SMS, Push)

Abandoned carts are the low-hanging fruit of ecommerce: people who were close to buying but left. Most stores rely on email, and emails are great — but you can significantly bump recoveries by building a multi-channel strategy that includes SMS and browser push. In this post I’ll walk you through a practical, Magento 2-focused approach covering third-party SMS APIs (Twilio, MessageBird), native browser push with Service Workers, personalization, automation triggers, and how to measure ROI with Google Analytics. Think of this like a hands-on conversation — I’ll show code, examples, and sensible defaults so you can implement this in a real store (and adapt to magefine hosting or extensions later).

Why go beyond email?

Quick bullet reasons:

  • SMS has a higher open rate (and immediate attention) than email — great for short recovery nudges.
  • Push notifications are great for logged-out visitors and mobile web users — they’re opt-in and persistent.
  • Combining channels lets you tailor cadence and message format — e.g., short SMS+link for immediate recovery, then richer email with product images and incentives.
  • Some shoppers ignore email but respond to SMS or push. More channels = more chances to convert.

High-level architecture

Here’s a simple architecture to implement in Magento 2:

  1. Identify abandoned carts (cron): run a job to find active quotes older than X hours with items but no order.
  2. Classify & segment: apply rules by cart value, product categories, user behavior (visited pages, coupon history).
  3. Personalize message templates: inject top product, cart value, dynamic coupon code.
  4. Deliver via channel: SMS (Twilio/MessageBird) or Push (Service Worker + web-push VAPID) or fallback to email.
  5. Track clicks / recoveries with tagged URLs and Google Analytics events.
  6. Automate workflows with temporal & behavioral triggers and stop after conversion.

Part 1 — Finding abandoned carts in Magento 2

Core idea: use the quote table (Magento quote) to find active carts that haven’t been converted into orders. Typical criteria:

  • is_active = 1
  • items_count > 0
  • updated_at older than threshold (1 hour, 6 hours, 24 hours)
  • no corresponding sales_order

Cron is the right place for periodic checks. Example snippet of a Magento 2 cron class that locates candidate quotes (simplified):

<?php
namespace Vendor\AbandonedCart\Cron;

use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory;
use Psr\Log\LoggerInterface;

class FindAbandonedCarts
{
    private $quoteCollectionFactory;
    private $logger;

    public function __construct(
        QuoteCollectionFactory $quoteCollectionFactory,
        LoggerInterface $logger
    ) {
        $this->quoteCollectionFactory = $quoteCollectionFactory;
        $this->logger = $logger;
    }

    public function execute()
    {
        // threshold example: updated more than 2 hours ago
        $threshold = (new \DateTime())->modify('-2 hours')->format('Y-m-d H:i:s');

        $collection = $this->quoteCollectionFactory->create()
            ->addFieldToFilter('is_active', 1)
            ->addFieldToFilter('items_count', ['gt' => 0])
            ->addFieldToFilter('updated_at', ['lt' => $threshold]);

        foreach ($collection as $quote) {
            // do a quick conversion check or push to queue for processing
            $this->logger->info('Found possible abandoned cart: ' . $quote->getId());
        }

        return $this;
    }
}

From here you can push quote IDs into a job queue or process them immediately to decide channel and message.

Part 2 — Integrating SMS: Twilio and MessageBird with Magento 2

SMS is super effective for short, time-sensitive messages. Below are two realistic integrations for Magento 2: Twilio and MessageBird. In each case you’ll create a small Magento 2 service that wraps the provider SDK and expose an admin config to store API keys.

1) Twilio (PHP SDK)

Install Twilio PHP SDK via composer in your Magento project (inside your repo):

composer require twilio/sdk

Create a service class (Vendor/AbandonedCart/Model/Sms/TwilioClient.php):

<?php
namespace Vendor\AbandonedCart\Model\Sms;

use Twilio\Rest\Client;
use Psr\Log\LoggerInterface;

class TwilioClient
{
    private $client;
    private $from;
    private $logger;

    public function __construct($sid, $token, $from, LoggerInterface $logger)
    {
        $this->client = new Client($sid, $token);
        $this->from = $from;
        $this->logger = $logger;
    }

    public function send($to, $message)
    {
        try {
            $this->client->messages->create($to, [
                'from' => $this->from,
                'body' => $message,
            ]);
            return true;
        } catch (\Exception $e) {
            $this->logger->error('Twilio send failed: ' . $e->getMessage());
            return false;
        }
    }
}

From your cron processor, call this service with a personalized message and a recovery link (see personalization later). Keep the SMS message under 160 characters or handle multi-part SMS costs.

2) MessageBird

MessageBird is similar; install their PHP library:

composer require messagebird/php-rest-api
<?php
namespace Vendor\AbandonedCart\Model\Sms;

use MessageBird\Client;
use Psr\Log\LoggerInterface;

class MessageBirdClient
{
    private $client;
    private $originator;
    private $logger;

    public function __construct($accessKey, $originator, LoggerInterface $logger)
    {
        $this->client = new Client($accessKey);
        $this->originator = $originator;
        $this->logger = $logger;
    }

    public function send($to, $message)
    {
        try {
            $messageObject = new \MessageBird\Objects\Message();
            $messageObject->originator = $this->originator;
            $messageObject->recipients = [$to];
            $messageObject->body = $message;

            $this->client->messages->create($messageObject);
            return true;
        } catch (\Exception $e) {
            $this->logger->error('MessageBird send failed: ' . $e->getMessage());
            return false;
        }
    }
}

Admin config: add admin fields for SID / token / originator so non-devs can update credentials.

Best practices for SMS

  • Always require explicit opt-in for marketing SMS (GDPR/CTIA rules). For abandoned-cart transactional messages some regions allow them if they’re operational, but check local rules.
  • Use short URLs (or UTM-tagged full URLs) that point back to the store and restore the cart (deep link that loads quote ID).
  • Avoid sending too frequently; typical cadence: 1st SMS after 1-2 hours, 2nd after 24 hours (if no conversion), optionally final 48–72 hrs with small incentive.

Part 3 — Browser Push: Service Workers, subscriptions, and sending notifications

Browser push is great for both logged-in and anonymous users (if they opt-in). The flow:

  1. Front-end registers a Service Worker and requests push permission.
  2. On subscribe, the browser provides a subscription object (JSON with endpoint + keys).
  3. Save subscription server-side (linked to customer or guest quote token).
  4. When an abandoned cart is eligible, send a push notification to the saved subscription via VAPID-signed web-push requests.

Service Worker (sw.js)

// sw.js
self.addEventListener('push', function(event) {
  const data = event.data ? event.data.json() : { title: 'Cart reminder', body: 'You left something in your cart' };
  const options = {
    body: data.body,
    icon: '/favicon-192.png',
    badge: '/badge-72.png',
    data: data.url
  };
  event.waitUntil(self.registration.showNotification(data.title, options));
});

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  const url = event.notification.data || '/';
  event.waitUntil(clients.openWindow(url));
});

Front-end subscription code (register & subscribe)

if ('serviceWorker' in navigator && 'PushManager' in window) {
  navigator.serviceWorker.register('/sw.js').then(async function(reg) {
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') return;

    const subscription = await reg.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array('')
    });

    // send subscription to server
    await fetch('/rest/V1/abandonedcart/save-subscription', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ subscription })
    });
  });
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

Server-side: sending push with PHP (Minishlink/web-push)

Install web-push library:

composer require minishlink/web-push
<?php
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

$auth = [
    'VAPID' => [
        'subject' => 'mailto:ops@yourdomain.com',
        'publicKey' => 'YOUR_VAPID_PUBLIC',
        'privateKey' => 'YOUR_VAPID_PRIVATE',
    ],
];

$webPush = new WebPush($auth);

$subscription = Subscription::create(json_decode($savedSubscriptionJson, true));
$payload = json_encode([
    'title' => 'You left items in your cart',
    'body' => 'Tap to complete your purchase — we reserved your cart for 24 hours',
    'url' => 'https://yourstore.com/checkout/cart/?quote=' . $quoteId . '&utm_source=push&utm_medium=abandoned_cart'
]);

$report = $webPush->sendOneNotification($subscription, $payload);

// handle report / clean subscriptions if expired

Linking subscriptions to carts

When the front-end sends the subscription JSON, store it with the customer_id (if logged in) or with the quote token for guests. That way your cron can look up the relevant subscription for the cart and send a targeted push.

Part 4 — Personalizing messages based on behavior & cart value

Personalization drives higher conversion. Keep the content simple for SMS and more descriptive for email. Personalization variables to consider:

  • Cart value (grand total)
  • Top product(s) — first item name, SKU, image URL
  • Customer name / last seen page
  • Number of items
  • Behavioral signals: coupon applied previously, viewed product page, high-intent events (checkout started)

Example: building a simple message template

$templateSms = "Hey {name}, you left {items_count} item(s) in your cart (Total: {cart_total}). Complete order: {link}";

$vars = [
  'name' => $customerName ?: 'there',
  'items_count' => $quote->getItemsCount(),
  'cart_total' => $quote->getGrandTotal(),
  'link' => $this->buildRecoveryLink($quote)
];

$message = strtr($templateSms, array_combine(array_map(function($k){return '{'.$k.'}';}, array_keys($vars)), array_values($vars)));

echo $message;

For emails you can render product images and add a limited-time coupon in the template. Generate coupon codes on demand using Magento's coupon APIs and insert them into the message if you want to incentivize high-value carts.

Segmentation examples

  • High-value carts (> $150): send a personalized SMS within 1 hour + email with 10% coupon after 24 hours if not converted.
  • Low-value carts (< $50): wait 6–12 hours, send push first (if subscribed), email later.
  • Product-specific: if cart contains fragile/expensive categories, include trust and shipping info in the message.

Part 5 — Automation workflows and triggers

Two types of triggers are useful:

  1. Temporal triggers: after 1 hour, after 24 hours, 72 hours.
  2. Behavioral triggers: user returned to product page, applied coupon, abandoned during checkout, or cross-device activity.

Implementing workflows: build a simple state machine for each quote with states such as NEW, SENT_SMS_1, SENT_PUSH_1, SENT_EMAIL_1, RECOVERED, EXPIRED. Your cron or queue worker updates state when sending messages and checks for recovery (order placed) to stop further messaging.

// pseudo-workflow
if (quote.state == 'NEW' && quote.updated_at < 1 hour ago) {
  if (quote.customer_phone && !opted_out_sms) sendSms(quote);
  if (pushSubscriptionExists(quote)) sendPush(quote);
  quote.setState('SENT_1');
}

if (quote.state == 'SENT_1' && quote.updated_at < 24 hours ago && cartValue > 150) {
  // send coupon + email
  sendEmailWithCoupon(quote);
  quote.setState('SENT_2');
}

Behavioural trigger example: if the user returns and views product >= 3 times after abandonment, escalate with SMS. You can track behavioral events via a front-end tracker that stores counts on the quote (via AJAX) or by tracking page views in your analytics and triggering via webhook.

Part 6 — Measuring effectiveness & ROI with Google Analytics

Measuring the performance of each channel (SMS vs push vs email) is critical. Use tagged recovery links, GA events, and ecommerce revenue data to compute recovery rate, incremental revenue and ROI.

1) Tagging recovery links

In your messages include URLs that restore the cart and include UTM parameters:

https://yourstore.com/checkout/cart/?quote=12345&utm_source=sms&utm_medium=abandoned_cart&utm_campaign=abandoned_2025_05

For push, add utm_source=push; for email, utm_source=email. This will allow GA to attribute the order to the proper channel.

2) GA4 implementation: event and ecommerce tagging

When a tagged link is clicked and leads to an order, ensure the order event is recorded in GA with purchase value and order_id. In GA4 you will see revenue attributed to the session that had the UTM tags (first non-direct click rules apply).

// example: fire a GA4 event on restored cart landing page
window.gtag('event', 'abandoned_cart_recovery_click', {
  method: 'sms', // sms | push | email
  quote_id: '12345'
});

When purchase completes, make sure the ecommerce 'purchase' event is sent with order value and order_id so GA can tie revenue back to the session that had the UTM.

3) Calculating ROI

Define the costs:

  • SMS cost per message (e.g., $0.01 – depends on region)
  • Push cost (usually free besides dev/hosting time)
  • Email cost (ESP fees allocated per message)

Basic ROI calculation for a period:

recovery_revenue_channel = SUM(revenue attributed to channel)
cost_channel = number_of_messages * unit_cost
roi = (recovery_revenue_channel - cost_channel) / cost_channel

Example: SMS campaign sent 10,000 messages, 1% recovered orders, average order value $80

  • Recovered orders = 100
  • Revenue = 100 * 80 = $8,000
  • Cost = 10,000 * $0.02 = $200
  • ROI = (8,000 - 200) / 200 = 38x

That’s simple math but you must be careful to attribute correctly: if an email click later converted, GA rules might attribute differently. Using first touch UTM on recovery click is recommended when measuring the immediate effect of that channel.

4) Use BigQuery or GA4 Explorations for deeper analysis

Export GA4 to BigQuery (if available) and join purchase events with campaign UTM to build a precise attribution table. Track metrics such as:

  • Recovery rate per channel
  • AOV per recovered order
  • Cost per recovered order
  • Net profit from recovered orders (subtract coupon discounts and message costs)

Part 7 — Practical examples and full flow

Let’s stitch everything together with a simple example flow for a non-logged-in shopper:

  1. User adds items to cart and leaves. A guest quote is created with quote_id=12345.
  2. Front-end asks permission for push. User accepts; subscription saved connected to quote token.
  3. After 2 hours cron finds quote 12345 as abandoned and checks: push subscription present, no phone number. The system sends a push with a deep link: https://yourstore.com/checkout/cart/?quote=12345&utm_source=push&utm_medium=abandoned_cart
  4. User clicks push within 30 minutes and completes order. GA logs purchase with utm_source=push and revenue is attributed to push campaign.
  5. If the user hadn’t clicked push, the workflow would wait 24 hours and then send an email with dynamic images and 5% coupon for carts over $100.

Example: PHP snippet to create a deep-recovery link

private function buildRecoveryLink($quoteId, $channel)
{
    $base = 'https://yourstore.com/checkout/cart/';
    $utm = sprintf('?quote=%s&utm_source=%s&utm_medium=abandoned_cart', $quoteId, $channel);
    return $base . $utm;
}

Part 8 — Privacy, compliance, and deliverability

Two important non-functional items:

  • Consent: Always ensure you have proper consent for marketing SMS/push. For transactional (order-related) SMS some regions allow them without explicit marketing consent but check local law.
  • Unsubscribe: Provide easy opt-out for SMS (e.g., reply STOP) and allow revocation of push subscriptions via site settings.

Deliverability tips:

  • Use a reputable provider (Twilio/MessageBird) and set up proper sender IDs and registration where required.
  • Monitor bounce and unsubscribe rates; prune bad numbers and expired push subscriptions.
  • For push, handle push endpoint expirations and remove invalid subscriptions from DB.

Part 9 — Operational tips and recommended cadence

Suggested starting cadence (you can A/B test):

  • 0h: no message (give a short cooling-off)
  • 1–2h: Push (if opted in) + short SMS (if phone available) — friendly reminder
  • 24h: Email with product images and suggested urgency — optionally a small coupon for high-value carts
  • 72h: Final SMS or email with last-chance coupon (if cost justified)

Always include a stop condition: when order is created for the quote, stop sending messages, and mark the workflow as RECOVERED.

Part 10 — A/B tests and KPIs

Run experiments on:

  • Channel order: SMS first vs push first
  • Timing: 1 hour vs 6 hours vs 24 hours
  • Incentives: percentage coupon vs free shipping vs no coupon

KPIs to track:

  • Recovery rate (orders / abandoned carts)
  • Recovered revenue
  • Cost per recovered order
  • Net margin after coupon cost and message cost
  • Subscriber opt-out rate

Closing notes and next steps

If you’re running Magento 2 on magefine hosting, think about the operational side: put cron workers on reliable nodes, secure your API keys in env or Magento config with proper ACL, and ensure your queue workers can scale during a peak. Start with a small test segment and measure impact before broad rollout.

Summary checklist before launching:

  • Implement quote finder cron and workflow state machine.
  • Add Twilio / MessageBird wrappers and admin config.
  • Implement Service Worker + subscription endpoint + push send logic.
  • Personalization templates and coupon integration for high-value carts.
  • UTM tagging and GA4 purchase event validation.
  • GDPR / consent and opt-out flows for SMS/push.
  • Monitoring and cleanup for expired subscriptions / invalid phone numbers.

If you want, I can help sketch the full Magento 2 module structure with files (module.xml, di.xml, etc.), or provide a ready-to-adapt sample repository layout you can drop into a magefine-hosted store. Want that next?

Keywords to keep in mind for SEO: Magento 2 abandoned cart recovery, SMS Twilio Magento, MessageBird Magento 2, browser push Service Worker Magento, abandoned cart automation, measure abandoned cart ROI, magefine.