---
title: "Offer targeting for retailers | Snowplow Signals"
description: "Snowplow Signals tells your storefront which shopper keeps returning to a price without buying, so your offer rules give the code to them and not to the rest."
source: https://signals.snowplow.io/solutions/retail/offers
---

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

# A discount that goes to the shopper who needs it. 

Signals tells your storefront when a shopper keeps coming back to a price without buying, and when one has added at full price, so your offer rules can give the code to the first and hold it from the second.

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

Lower discount spend

## Spend less on discounts by giving the code to the shopper who is hesitating. 

Discount spend falls when the code reaches the shopper who is weighing the price and stays away from the one who was going to pay in full. Signals gives your storefront how many times this shopper has come back to a product this visit and across visits, whether they have added anything to the cart, and whether a coupon is on the order. Your offer rules decide who gets a code, how large it is, and who gets nothing.

Four ways retailers use it

01 The shopper circling one price Third look at the same jacket this visit, cart empty. Give the code to them. 02 The shopper who added at full price One look, straight into the cart at full price. No code at all. 03 The browser who keeps coming back Same boots on three visits, never added. A smaller nudge first. 04 The shopper who arrived with a code In from a coupon site, code applied, full cart. Not a second one. 

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

### Give the code to the shopper who keeps coming back to the price

A shopper has opened the same jacket three times this visit, scrolled to the price each time and left the page without adding it. The site-wide code went out in this morning's email to everyone, before anyone could tell this shopper from the one who bought on the first look. The third return to the price is the moment to make the offer. The rules can only make it then if they see it while the shopper is still on the product page.

What Signals knows

`product_views_session` 

How many times they opened each product this visit

`adds_session` 

Items added to the cart this visit

What your offer rules do

Shows a code on the product page once a shopper has opened the same product three times this visit with nothing in the cart, and keeps the site-wide banner closed for everyone who has not.

Define it · PythonUse it · TypeScript

product.tsx · 13 lines Copy Copied 

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

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

product_views_session = Attribute(
    name="product_views_session",
    type="dict",
    events=[page_view],
    aggregation="category_count",
    property=product_id,
)

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

# Keyed on the visit, so the count resets when the shopper comes back another day.
shop_offers_hesitating = StreamAttributeGroup(
    name="shop_offers_hesitating",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[product_views_session, adds_session],
)
```

```
// In the code that renders the product page
const shopper = await signals.getServiceAttributes({
  name: "shop_offers_hesitating",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { product_views_session: { "sku-4471": 3, "sku-2210": 1 }, adds_session: 0 }

const looks = shopper.product_views_session[productId] ?? 0;

if (looks >= 3 && shopper.adds_session === 0) {
  offers.show({ kind: "code", product: productId, placement: "price" });
}
```

### Hold the code from the shopper who has decided

A shopper landed on a product from search, read the page once and added it to the cart at full price. The site-wide code reaches them anyway, in the banner, in the cart and again at checkout, because it went out before anyone could tell them from the shopper who is still deciding. The discount lands on a sale that was going through without it, and the margin goes with it.

What Signals knows

`adds_session` 

Items added to the cart this visit

`products_viewed_session` 

How many different products they opened this visit

What your offer rules do

Keeps the code banner and the promo field closed for a shopper who added on the first look, and lets the order go through at full price.

Define it · PythonUse it · TypeScript

product.tsx · 11 lines Copy Copied 

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

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

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

products_viewed_session = Attribute(
    name="products_viewed_session",
    type="int32",
    events=[page_view],
    aggregation="approx_count_distinct",
    property=EntityProperty(vendor="com.example", name="product", major_version=1, path="id"),
)

shop_offers_decided = StreamAttributeGroup(
    name="shop_offers_decided",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[adds_session, products_viewed_session],
)
```

```
// In the code that renders the product page and the cart
const shopper = await signals.getServiceAttributes({
  name: "shop_offers_decided",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { adds_session: 1, products_viewed_session: 1 }

if (shopper.adds_session >= 1 && shopper.products_viewed_session <= 1) {
  offers.suppress({ scope: "session", reason: "added_at_full_price" });
}
```

### Start with a smaller nudge for a shopper who keeps coming back

A shopper has opened the same pair of boots on three visits over two weeks and has not added them once. The retargeting ad and the browse-abandon email both carry the full code, so the first touch spends the whole discount without knowing whether this shopper would have moved for less. Free delivery or a low-stock note is the smaller ask. The code waits until they come back once more and stall again.

What Signals knows

`product_views_30d` 

How many times they opened each product in the last 30 days

`visits_30d` 

Separate visits in the last 30 days

`adds_30d` 

Items added to the cart in the last 30 days

What your offer rules do

Shows free delivery on the product page when a shopper has opened the same product across three visits without adding it, and moves to a code only when they come back a fourth time and still have not added it.

Define it · PythonUse it · TypeScript

product.tsx · 14 lines Copy Copied 

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

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

product_views_30d = Attribute(
    name="product_views_30d",
    type="dict",
    events=[page_view],
    aggregation="category_count",
    property=EntityProperty(vendor="com.example", name="product", major_version=1, path="id"),
    period=timedelta(days=30),
)

visits_30d = Attribute(
    name="visits_30d",
    type="int32",
    events=[page_view],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="domain_sessionid"),
    period=timedelta(days=30),
)

adds_30d = Attribute(
    name="adds_30d",
    type="int32",
    events=[add_to_cart],
    aggregation="counter",
    period=timedelta(days=30),
    default_value=0,
)

# Keyed on the browser, so the same shopper is recognized across visits without logging in.
shop_offers_returning = StreamAttributeGroup(
    name="shop_offers_returning",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[product_views_30d, visits_30d, adds_30d],
)
```

```
// In the code that renders the product page
const shopper = await signals.getServiceAttributes({
  name: "shop_offers_returning",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { product_views_30d: { "sku-8812": 3 }, visits_30d: 3, adds_30d: 0 }

const looks = shopper.product_views_30d[productId] ?? 0;

if (looks >= 3 && shopper.visits_30d >= 3 && shopper.adds_30d === 0) {
  const kind = shopper.visits_30d >= 4 ? "code" : "free_delivery";
  offers.show({ kind, product: productId, placement: "price" });
}
```

### Keep a second code off an order that has one

A shopper arrived from a coupon site, applied the code they found there and has filled a cart. The exit offer and the checkout banner were set up for a shopper about to leave empty-handed, and they fire on the way to checkout without knowing a code is on the order. Two discounts land on an order that was going through with one.

What Signals knows

`coupons_applied_session` 

Coupon codes applied this visit

`entered_from` 

The campaign medium on their first page view

`adds_session` 

Items added to the cart this visit

What your offer rules do

Closes the exit offer and the checkout code banner for a shopper who has a coupon on the order, and leaves the code they brought with them to stand alone.

Define it · PythonUse it · TypeScript

checkout.tsx · 11 lines Copy Copied 

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

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
add_to_cart = Event(name="add_to_cart", vendor="com.example", version="1-0-0")
coupon_applied = Event(name="coupon_applied", vendor="com.example", version="1-0-0")

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

entered_from = Attribute(
    name="entered_from",
    type="string",
    events=[page_view],
    aggregation="first",
    property=AtomicProperty(name="mkt_medium"),
)

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

shop_offers_stacked = StreamAttributeGroup(
    name="shop_offers_stacked",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[coupons_applied_session, entered_from, adds_session],
)
```

```
// In the code that renders checkout and the exit offer
const shopper = await signals.getServiceAttributes({
  name: "shop_offers_stacked",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { coupons_applied_session: 1, entered_from: "affiliate", adds_session: 4 }

if (shopper.coupons_applied_session > 0) {
  offers.suppress({ scope: "checkout", reason: "coupon_on_order", via: shopper.entered_from });
}
```

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