---
title: "Checkout prompts for retailers | Snowplow Signals"
description: "Snowplow Signals tells your storefront which checkout step a shopper is stuck on, for how long and what went wrong, so it can offer help on that step."
source: https://signals.snowplow.io/solutions/retail/checkout
---

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

# A checkout that notices when a shopper is stuck. 

Signals tells your storefront which step a shopper is on, how long they have been there and what has gone wrong, so it can offer help on that step while the shopper is still there.

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

Higher checkout conversion

## Convert more checkouts by helping a shopper on the step where they stall. 

Checkout conversion moves when the storefront can tell a stalled shopper from one still deciding, and acts on that step while the shopper is on it. Signals gives it the step the shopper is on, how long they have been there, the errors they have hit this visit and how many times they have gone back to the cart. The storefront decides what to show.

Four ways retailers use it

01 Shoppers stuck on the address step Four minutes on the address form, three errors. Offer a way around it. 02 Shoppers who go back to the cart from payment Back to the cart from payment, twice. Show the full total, not a code. 03 Shoppers whose payment failed One card declined on the payment step. Offer another way to pay. 04 Shoppers who keep reaching checkout without buying Third checkout this week, no order. Let your own rule decide. 

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

### Offer address lookup to a shopper the form keeps rejecting

A shopper has been on the address step for four minutes and the form has rejected it three times, each time on the postal code. From the outside this looks like a shopper taking their time. A recovery message sent after they leave arrives after the problem, and the problem was the form. What they need now is an address lookup, or a guest checkout that asks for less.

What Signals knows

`current_step` 

The checkout step the shopper is on now

`seconds_on_step` 

Seconds since the shopper reached this step

`address_errors_session` 

Validation errors on the address step this visit

What your storefront does

Swaps in address lookup once a shopper on the address step has had two errors, and offers guest checkout when the time on the step passes the limit your team set.

Define it · PythonUse it · TypeScript

checkout.tsx · 15 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Criteria, Criterion, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

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

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

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

address_errors_session = Attribute(
    name="address_errors_session",
    type="int32",
    events=[checkout_error],
    aggregation="counter",
    criteria=Criteria(all=[
        Criterion.eq(EventProperty(vendor="com.example", name="checkout_error", major_version=1, path="step"), "address"),
    ]),
    default_value=0,
)

# Keyed on the visit, so the error count starts again when the shopper comes back another day.
shop_checkout_address = StreamAttributeGroup(
    name="shop_checkout_address",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[current_step, seconds_on_step, address_errors_session],
)
```

```
// In the code that renders the checkout
const shopper = await signals.getServiceAttributes({
  name: "shop_checkout_address",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { current_step: "address", seconds_on_step: 243, address_errors_session: 3 }

if (shopper.current_step === "address" && shopper.address_errors_session >= 2) {
  checkout.show({ help: "address_lookup" });
}

if (shopper.current_step === "address" && shopper.seconds_on_step > checkout.stallAfter) {
  checkout.show({ help: "guest_checkout" });
}
```

### Show the full price to a shopper who keeps checking the cart

A shopper reached the payment step, went back to the cart, came forward, and has just gone back again. They are checking something, most often what the order costs with delivery and whether it can be returned. A discount code answers a question they did not ask and gives away margin. The storefront needs to know this is the second return while the cart page is rendering, so the total with delivery and the returns policy are the first things on it.

What Signals knows

`current_step` 

The checkout step the shopper is on now

`cart_returns_session` 

Times the shopper went back to the cart from payment this visit

What your storefront does

Puts the total with delivery, the delivery date and the returns policy at the top of the cart page on the second return from payment, and leaves the discount rules alone.

Define it · PythonUse it · TypeScript

checkout.tsx · 11 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Criteria, Criterion, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

checkout_step = Event(name="checkout_step", vendor="com.example", version="1-0-0")
step = EventProperty(vendor="com.example", name="checkout_step", major_version=1, path="step")
previous_step = EventProperty(vendor="com.example", name="checkout_step", major_version=1, path="previous_step")

current_step = Attribute(
    name="current_step",
    type="string",
    events=[checkout_step],
    aggregation="last",
    property=step,
)

cart_returns_session = Attribute(
    name="cart_returns_session",
    type="int32",
    events=[checkout_step],
    aggregation="counter",
    criteria=Criteria(all=[
        Criterion.eq(step, "cart"),
        Criterion.eq(previous_step, "payment"),
    ]),
    default_value=0,
)

shop_checkout_cart = StreamAttributeGroup(
    name="shop_checkout_cart",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[current_step, cart_returns_session],
)
```

```
// In the code that renders the cart page
const shopper = await signals.getServiceAttributes({
  name: "shop_checkout_cart",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { current_step: "cart", cart_returns_session: 2 }

if (shopper.current_step === "cart" && shopper.cart_returns_session >= 2) {
  cart.render({ lead: ["total_with_delivery", "delivery_date", "returns_policy"] });
}
```

### Offer another payment method after the first one fails

A shopper's card was declined a moment ago and they are still on the payment step. Many will try the same card once more and then leave. The payment page shows the same form it showed before the failure, and the storefront learns about the decline after the shopper has gone. The moment to offer a wallet or pay later is now, on this step, before the second try.

What Signals knows

`current_step` 

The checkout step the shopper is on now

`payment_failures_session` 

Payment attempts that failed this visit

`last_payment_method` 

The payment method the shopper tried last

What your storefront does

Moves the other payment methods above the card form after one failure, with the method the shopper has not tried first, and keeps the card form on the page for a shopper who wants to retry.

Define it · PythonUse it · TypeScript

checkout.tsx · 11 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Criteria, Criterion, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

checkout_step = Event(name="checkout_step", vendor="com.example", version="1-0-0")
checkout_error = Event(name="checkout_error", vendor="com.example", version="1-0-0")
error_step = EventProperty(vendor="com.example", name="checkout_error", major_version=1, path="step")

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

payment_failures_session = Attribute(
    name="payment_failures_session",
    type="int32",
    events=[checkout_error],
    aggregation="counter",
    criteria=Criteria(all=[Criterion.eq(error_step, "payment")]),
    default_value=0,
)

last_payment_method = Attribute(
    name="last_payment_method",
    type="string",
    events=[checkout_error],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="checkout_error", major_version=1, path="payment_method"),
    criteria=Criteria(all=[Criterion.eq(error_step, "payment")]),
)

shop_checkout_payment = StreamAttributeGroup(
    name="shop_checkout_payment",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[current_step, payment_failures_session, last_payment_method],
)
```

```
// In the code that renders the payment step
const shopper = await signals.getServiceAttributes({
  name: "shop_checkout_payment",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { current_step: "payment", payment_failures_session: 1, last_payment_method: "card" }

if (shopper.current_step === "payment" && shopper.payment_failures_session >= 1) {
  checkout.orderPaymentMethods({ demote: shopper.last_payment_method });
}
```

### Treat a third checkout this week differently from a first

This shopper has reached checkout three times this week and has not placed an order. Each visit the storefront greets them as if it were the first. Whether they are comparing prices, waiting for payday or waiting on a stock date is something your team will know better than any default. What the storefront needs is to know this is the third time, while the checkout is rendering, so your rule can act on it. Perhaps a saved-cart reminder, perhaps nothing.

What Signals knows

`checkout_visits_7d` 

Separate visits that reached checkout in the last seven days

`orders_7d` 

Orders placed in the last seven days

What your storefront does

Hands the count to your own rule. One storefront offers to save the cart and send a reminder, another shows the delivery date, another waits. The rule is yours.

Define it · PythonUse it · TypeScript

checkout.tsx · 11 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, AtomicProperty, Event, StreamAttributeGroup, domain_userid

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

checkout_visits_7d = Attribute(
    name="checkout_visits_7d",
    type="int32",
    events=[checkout_step],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="domain_sessionid"),
    period=timedelta(days=7),
)

orders_7d = Attribute(
    name="orders_7d",
    type="int32",
    events=[order_placed],
    aggregation="counter",
    period=timedelta(days=7),
    default_value=0,
)

# Keyed on the browser, not the visit, so the count carries across the week.
shop_checkout_repeat = StreamAttributeGroup(
    name="shop_checkout_repeat",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[checkout_visits_7d, orders_7d],
)
```

```
// In the code that renders the first checkout step
const shopper = await signals.getServiceAttributes({
  name: "shop_checkout_repeat",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { checkout_visits_7d: 3, orders_7d: 0 }

if (shopper.checkout_visits_7d >= 3 && shopper.orders_7d === 0) {
  checkout.apply(rules.repeatCheckout({ visits: shopper.checkout_visits_7d }));
}
```

## 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)
