---
title: "Cart recovery for retailers | Snowplow Signals"
description: "Snowplow Signals tells your storefront and messaging tool when a cart sits idle, stalls at shipping or comes back days later, so they act during the visit."
source: https://signals.snowplow.io/solutions/retail/cart
---

[Retail and e-commerce](https://signals.snowplow.io/solutions/retail) / Cart recovery 

# Recover the cart while the shopper is still here. 

Signals tells your storefront and your messaging tool how much is in a shopper's cart, how long it has sat untouched and how far through checkout they got, so the nudge reaches them during the visit rather than in tomorrow's inbox.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

Higher cart conversion

## Turn more full carts into orders by nudging the shopper during the visit. 

Cart conversion moves when the nudge reaches a shopper who is still on the site. Signals gives your storefront and your messaging tool the value of the cart, how long it has sat untouched, which checkout step the shopper reached and what they have added and taken out this visit. Each one decides whether to show a line, hold an offer back or restore a cart, and when.

Four ways retailers use it

01 A full cart left idle Three items in, nothing touched for ten minutes, still browsing. Nudge on the page. 02 A high-value cart stuck at shipping Large cart, four minutes on the shipping step. Answer the delivery question now. 03 A shopper adding and removing the same item Same jacket added twice, removed twice. Reassure, do not discount. 04 A returning browser with a full cart Back after three days, cart still full, no order placed. Restore it and say so. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Nudge the shopper on the page while the cart sits idle

A shopper has three items in the cart and has spent the last ten minutes in another category without touching it. The recovery email is written for the shopper who has gone, so it waits for the visit to end and lands beside everything else in the inbox. This shopper is still on the site. The nudge belongs in the header, next to the cart, while they can see it.

What Signals knows

`cart_value_session` 

Value of what has been added to the cart this visit

`seconds_since_cart_change` 

Seconds since the cart was last added to or taken from

`cart_adds_session` 

Items added to the cart this visit

What your storefront and messaging tool do

Shows a line beside the cart icon once a cart above the value your team sets has gone untouched for ten minutes, and tells your messaging tool the visit is still going so the recovery email waits.

Define it · PythonUse it · TypeScript

header.tsx · 14 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

add_to_cart = Event(name="add_to_cart", vendor="com.example", version="1-0-0")
remove_from_cart = Event(name="remove_from_cart", vendor="com.example", version="1-0-0")

cart_value_session = Attribute(
    name="cart_value_session",
    type="double",
    events=[add_to_cart],
    aggregation="sum",
    property=EventProperty(vendor="com.example", name="add_to_cart", major_version=1, path="price"),
    default_value=0,
)

seconds_since_cart_change = Attribute(
    name="seconds_since_cart_change",
    type="double",
    events=[add_to_cart, remove_from_cart],
    aggregation="time_since_last",
    time_unit="s",
)

cart_adds_session = Attribute(
    name="cart_adds_session",
    type="int32",
    events=[add_to_cart],
    aggregation="counter",
    default_value=0,
)

# Most shoppers are anonymous, so this group keys on the visit.
shop_cart_idle = StreamAttributeGroup(
    name="shop_cart_idle",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[cart_value_session, seconds_since_cart_change, cart_adds_session],
)
```

```
// In the code that renders the storefront header
const cart = await signals.getServiceAttributes({
  name: "shop_cart_idle",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { cart_value_session: 142.5, seconds_since_cart_change: 640, cart_adds_session: 3 }

if (cart.cart_adds_session > 0 && cart.cart_value_session >= rules.nudgeAt && cart.seconds_since_cart_change >= 600) {
  header.show({
    beside: "cart",
    text: `${cart.cart_adds_session} items are waiting in your cart`,
  });
}
```

### Answer the delivery question on the step where it is asked

A shopper with a large cart reached the shipping step four minutes ago and has not moved on. From outside the visit a stalled checkout looks like any other open cart, so the follow-up arrives after the tab is closed, and the question that stopped them goes unanswered in the meantime. On the shipping step the question is usually when the order arrives or what delivery costs. The answer can sit on the step itself.

What Signals knows

`cart_value_session` 

Value of what has been added to the cart this visit

`checkout_step_session` 

The last checkout step the shopper reached this visit

`seconds_on_step` 

Seconds since they reached that step

What your storefront and messaging tool do

Shows the delivery date for the shopper's address, or how far the cart is from free shipping, once a cart above your threshold has sat on the shipping step past the limit your team sets. Which line it shows comes from your rules.

Define it · PythonUse it · TypeScript

checkout.tsx · 19 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

add_to_cart = Event(name="add_to_cart", vendor="com.example", version="1-0-0")
checkout_step = Event(name="checkout_step", vendor="com.example", version="1-0-0")

cart_value_session = Attribute(
    name="cart_value_session",
    type="double",
    events=[add_to_cart],
    aggregation="sum",
    property=EventProperty(vendor="com.example", name="add_to_cart", major_version=1, path="price"),
    default_value=0,
)

checkout_step_session = Attribute(
    name="checkout_step_session",
    type="string",
    events=[checkout_step],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="checkout_step", major_version=1, path="step"),
    default_value="none",
)

seconds_on_step = Attribute(
    name="seconds_on_step",
    type="double",
    events=[checkout_step],
    aggregation="time_since_last",
    time_unit="s",
)

shop_cart_shipping = StreamAttributeGroup(
    name="shop_cart_shipping",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[cart_value_session, checkout_step_session, seconds_on_step],
)
```

```
// In the code that renders the checkout steps
const cart = await signals.getServiceAttributes({
  name: "shop_cart_shipping",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { cart_value_session: 312.0, checkout_step_session: "shipping", seconds_on_step: 240 }

const stalled =
  cart.checkout_step_session === "shipping" &&
  cart.cart_value_session >= rules.highValue &&
  cart.seconds_on_step >= rules.stallSeconds;

if (stalled) {
  checkout.show({
    line: cart.cart_value_session >= rules.freeShippingAt ? "delivery_date" : "free_shipping_gap",
    gap: Math.max(0, rules.freeShippingAt - cart.cart_value_session),
  });
}
```

### Reassure a shopper who keeps changing their mind

A shopper has added the same jacket to the cart twice this visit and taken it out twice. An exit offer reads that as a price objection and spends a discount on it. Going back and forth on one item usually means a question about fit, stock or returns, and the answer should arrive on the product page the second time the jacket comes out of the cart.

What Signals knows

`adds_by_product_session` 

Times each product was added to the cart this visit

`removes_by_product_session` 

Times each product was taken out of the cart this visit

What your storefront and messaging tool do

Shows the stock level, the fit note and the returns window on the product page once one product has gone in and out of the cart twice, and leaves the discount code where it is.

Define it · PythonUse it · TypeScript

product.tsx · 19 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, EntityProperty, Event, StreamAttributeGroup, domain_sessionid,
)

add_to_cart = Event(name="add_to_cart", vendor="com.example", version="1-0-0")
remove_from_cart = Event(name="remove_from_cart", vendor="com.example", version="1-0-0")
product = EntityProperty(vendor="com.example", name="product", major_version=1, path="id")

adds_by_product_session = Attribute(
    name="adds_by_product_session",
    type="dict",
    events=[add_to_cart],
    aggregation="category_count",
    property=product,
)

removes_by_product_session = Attribute(
    name="removes_by_product_session",
    type="dict",
    events=[remove_from_cart],
    aggregation="category_count",
    property=product,
)

shop_cart_hesitating = StreamAttributeGroup(
    name="shop_cart_hesitating",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[adds_by_product_session, removes_by_product_session],
)
```

```
// In the code that renders the product page
const cart = await signals.getServiceAttributes({
  name: "shop_cart_hesitating",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { adds_by_product_session: { "jk-2210": 2, "sc-114": 1 }, removes_by_product_session: { "jk-2210": 2 } }

const adds = cart.adds_by_product_session[productId] ?? 0;
const removes = cart.removes_by_product_session[productId] ?? 0;

if (adds >= 2 && removes >= 2) {
  productPage.show({
    panel: "reassurance",
    stock: true,
    fitNote: true,
    returnsWindow: true,
  });
}
```

### Restore the cart and say so when the shopper comes back

A shopper who built a cart on Tuesday is back on Friday, as anonymous as they were then, and lands on the homepage. The cart may be intact in your storefront, but nothing on the page says so. The recovery email needs an address, and a shopper who never signed in has not given one, so the visit is the moment that reaches them. The first screen should say the cart is waiting and open it.

What Signals knows

`days_since_cart_change` 

Days since the cart was last added to or taken from

`cart_items_30d` 

The products added to the cart in the last 30 days

`last_checkout_step` 

The last checkout step reached, across visits

What your storefront and messaging tool do

Opens the homepage with a line that says the cart is still here when a shopper comes back a day or more after their last cart change without reaching the order confirmation, and leaves it out for the shopper whose last step was the confirmation.

Define it · PythonUse it · TypeScript

home.tsx · 20 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, EntityProperty, Event, EventProperty, StreamAttributeGroup, domain_userid,
)

add_to_cart = Event(name="add_to_cart", vendor="com.example", version="1-0-0")
remove_from_cart = Event(name="remove_from_cart", vendor="com.example", version="1-0-0")
checkout_step = Event(name="checkout_step", vendor="com.example", version="1-0-0")

days_since_cart_change = Attribute(
    name="days_since_cart_change",
    type="double",
    events=[add_to_cart, remove_from_cart],
    aggregation="time_since_last",
    time_unit="d",
)

cart_items_30d = Attribute(
    name="cart_items_30d",
    type="string_list",
    events=[add_to_cart],
    aggregation="unique_list",
    property=EntityProperty(vendor="com.example", name="product", major_version=1, path="id"),
    period=timedelta(days=30),
)

last_checkout_step = Attribute(
    name="last_checkout_step",
    type="string",
    events=[checkout_step],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="checkout_step", major_version=1, path="step"),
    ttl=timedelta(days=30),
    default_value="none",
)

# Keyed on the browser, not the visit, so the cart follows the shopper back.
shop_cart_returning = StreamAttributeGroup(
    name="shop_cart_returning",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[days_since_cart_change, cart_items_30d, last_checkout_step],
)
```

```
// In the code that renders the homepage
const shopper = await signals.getServiceAttributes({
  name: "shop_cart_returning",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { days_since_cart_change: 3.1, cart_items_30d: ["jk-2210", "sc-114"], last_checkout_step: "none" }

const cartWaiting =
  shopper.cart_items_30d.length > 0 &&
  shopper.days_since_cart_change >= 1 &&
  shopper.last_checkout_step !== "confirmation";

if (cartWaiting) {
  cart.restore();
  homepage.show({
    banner: "cart_waiting",
    text: "Your cart from the other day is still here",
  });
}
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
