---
title: "Paywall for publishers | Snowplow Signals"
description: "Snowplow Signals tells your paywall how often a reader comes back, what they read and what they just did, so it chooses when to ask and what to offer."
source: https://signals.snowplow.io/solutions/media/paywall
---

[Media and publishing](https://signals.snowplow.io/solutions/media) / Paywall 

# A paywall that can tell readers apart. 

Signals tells your paywall how often a reader comes back, what they read and what they just did, so it can choose when to ask and what to offer.

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

Higher subscription conversion

## Turn more readers into subscribers by asking at the right moment. 

Subscription conversion moves when the paywall can tell readers apart. Signals gives it how often a reader comes back, what they read and what they just did, so it can choose when to ask, whether to ask for an email or a payment, and which package to offer.

Four ways publishers use it

01 Readers who keep coming back Four visits this week, mostly climate. Ask now. 02 First-time readers In from search, two long reads. Ask for an email, not money. 03 Readers with a favorite section Lives in sport. Offer the sport package, not all-access. 04 Readers who walk away Hit the wall twice, no checkout. Offer a trial before they go. 

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

### Make the offer when a reader keeps coming back

A reader has visited four times this week, and most of what they read is climate coverage. A monthly meter either stopped them on the first visit or has not stopped them at all. This is the reader most likely to pay, and right now they are on the site.

What Signals knows

`visits_7d` 

Separate visits in the last seven days

`top_section_30d` 

The section they read most in the last 30 days

What your paywall does

Shows the subscription offer once a reader reaches three visits in a week, and names the section they keep reading in the headline of the offer.

Define it · PythonUse it · TypeScript

article.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")

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

top_section_30d = Attribute(
    name="top_section_30d",
    type="string",
    events=[page_view],
    aggregation="most_frequent",
    property=EntityProperty(vendor="com.example", name="article", major_version=1, path="section"),
    period=timedelta(days=30),
)

# Published as the paywall_returning service your article page reads.
paywall_returning = StreamAttributeGroup(
    name="paywall_returning",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[visits_7d, top_section_30d],
)
```

```
// In the code that renders the article page
const reader = await signals.getServiceAttributes({
  name: "paywall_returning",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { visits_7d: 4, top_section_30d: "climate" }

if (reader.visits_7d >= 3) {
  paywall.show({
    offer: "standard",
    headline: `Keep reading ${reader.top_section_30d} coverage`,
  });
}
```

### Ask a first-time reader for an email, not a card

A reader arrived from a search result an hour ago and has read two articles to the end. A meter that counts articles will show them the payment wall on the next one, before they have any reason to pay. They have shown interest, not a habit. The right ask is the newsletter.

What Signals knows

`visits_7d` 

Separate visits in the last seven days

`entered_from` 

The referrer medium on their first event

`long_reads_7d` 

Articles read to the end in the last seven days

What your paywall does

Swaps the payment wall for a newsletter sign-up when a first visit from search reaches two completed reads, and holds the subscription offer until the reader comes back.

Define it · PythonUse it · TypeScript

article.tsx · 14 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, AtomicProperty, Criteria, Criterion, Event, EventProperty, StreamAttributeGroup, domain_userid,
)

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

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

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

long_reads_7d = Attribute(
    name="long_reads_7d",
    type="int32",
    events=[article_read],
    aggregation="counter",
    criteria=Criteria(all=[
        Criterion.gte(EventProperty(vendor="com.example", name="article_read", major_version=1, path="percent_read"), 80),
    ]),
    period=timedelta(days=7),
    default_value=0,
)

paywall_first_visit = StreamAttributeGroup(
    name="paywall_first_visit",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[visits_7d, entered_from, long_reads_7d],
)
```

```
// In the code that renders the article page
const reader = await signals.getServiceAttributes({
  name: "paywall_first_visit",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { visits_7d: 1, entered_from: "search", long_reads_7d: 2 }

if (reader.visits_7d === 1 && reader.entered_from === "search" && reader.long_reads_7d >= 2) {
  paywall.show({
    offer: "newsletter",
    headline: "Get the next one by email",
  });
}
```

### Offer the package that matches what they read

Most of what this reader opens is sport, and has been for a month. The all-access offer prices in politics, culture and business they never read, so it reads as expensive. A sport pass is the same decision at a smaller price, and the headline can say so.

What Signals knows

`top_section_30d` 

The section they read most in the last 30 days

`sections_30d` 

How many articles they read in each section in the last 30 days

What your paywall does

Offers the section pass instead of all-access when one section is more than six in ten of a reader's articles this month, and names that section in the offer.

Define it · PythonUse it · TypeScript

article.tsx · 18 lines Copy Copied 

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

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
section = EntityProperty(vendor="com.example", name="article", major_version=1, path="section")

top_section_30d = Attribute(
    name="top_section_30d",
    type="string",
    events=[page_view],
    aggregation="most_frequent",
    property=section,
    period=timedelta(days=30),
)

sections_30d = Attribute(
    name="sections_30d",
    type="dict",
    events=[page_view],
    aggregation="category_count",
    property=section,
    period=timedelta(days=30),
)

paywall_section = StreamAttributeGroup(
    name="paywall_section",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[top_section_30d, sections_30d],
)
```

```
// In the code that renders the article page
const reader = await signals.getServiceAttributes({
  name: "paywall_section",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { top_section_30d: "sport", sections_30d: { sport: 19, news: 4, culture: 1 } }

const counts = Object.values(reader.sections_30d);
const total = counts.reduce((sum, n) => sum + n, 0);
const share = reader.sections_30d[reader.top_section_30d] / total;

if (share > 0.6) {
  paywall.show({
    offer: `${reader.top_section_30d}_pass`,
    headline: `All of our ${reader.top_section_30d} coverage, for less than all-access`,
  });
}
```

### Change the offer for a reader who has already said no

This reader has hit the paywall twice this month and never opened checkout. Showing the same wall a third time gets the same answer. The price is the objection, and a trial answers it while they are still on the page.

What Signals knows

`paywall_hits_30d` 

Times the paywall was shown in the last 30 days

`checkouts_30d` 

Times they opened checkout in the last 30 days

What your paywall does

Shows a trial offer instead of the standard wall once a reader has been stopped twice without opening checkout, and stops repeating the wall they have already refused.

Define it · PythonUse it · TypeScript

article.tsx · 14 lines Copy Copied 

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

paywall_shown = Event(name="paywall_shown", vendor="com.example", version="1-0-0")
checkout_opened = Event(name="checkout_opened", vendor="com.example", version="1-0-0")

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

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

paywall_walkaway = StreamAttributeGroup(
    name="paywall_walkaway",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[paywall_hits_30d, checkouts_30d],
)
```

```
// In the code that renders the article page
const reader = await signals.getServiceAttributes({
  name: "paywall_walkaway",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { paywall_hits_30d: 2, checkouts_30d: 0 }

if (reader.paywall_hits_30d >= 2 && reader.checkouts_30d === 0) {
  paywall.show({
    offer: "trial",
    headline: "Try a month, then decide",
  });
}
```

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