OnlyFans Mass Messaging That Converts: Segmentation, Timing, and Copy Frameworks
Mass messages to your full list convert at 2-4%. Segmented messages convert at 12-18%. We A/B tested across 50,000 messages and found exactly where the difference comes from — here's the full framework.
Our first year, mass messaging meant one message to everyone. We’d write what felt like a good hook, attach the PPV, send it at whatever time felt reasonable, and watch the purchases come in. We thought we were doing it right because the revenue looked fine in isolation. It wasn’t until we started testing — actually controlled A/B testing across 50,000 messages over eight weeks — that we understood how much headroom there was.
The result: we moved from 3.1% average conversion on mass PPV messages to 14.7% average, with our best segments hitting 22%. Same creators, same content, dramatically different revenue. Here’s exactly how we did it.
Why Unsegmented Mass Messaging Fails
The intuition behind blasting your full list is understandable. Everyone subscribed. Everyone could theoretically buy. Why not reach them all?
The problem is that your subscriber list is not a homogeneous group. It contains:
- Fans who tipped last week and are highly engaged right now
- Fans who subscribed 3 months ago, made one PPV purchase, and have been mostly dormant since
- Fans who subscribed on a promotional discount and have never spent anything beyond the sub fee
- Fans who are in the process of deciding whether to renew
- Fans who are power buyers — they’ll purchase almost anything offered to them
Sending the same message at the same price at the same time to all of these groups doesn’t just underperform. It actively damages some of them. The dormant fan who gets an aggressive PPV pitch the same week their sub is up for renewal is more likely to churn than to convert. The power buyer who gets a $10 offer is probably leaving $20 on the table.
Segmentation treats these groups differently because they are different. The framework below is built on three variables: recency of last purchase, total spend tier, and behavioral activity patterns.
The Three-Variable Segmentation Framework
Variable 1: Purchase Recency (The Most Important Signal)
When did this fan last make a purchase (PPV, tip, or paid unlock)? This single variable is more predictive of conversion than any copy or timing factor. A fan who bought something in the last 7 days converts at nearly 5x the rate of a fan who hasn’t bought anything in 60+ days.
We define four recency tiers:
- Hot (0–7 days): Recent purchaser, maximum receptivity. These fans are in an active spending relationship with the creator. They’re warm. They bought recently and the purchase experience was positive enough that they’re still subscribed.
- Warm (8–30 days): Made a purchase in the last month. Still engaged but not in active spending mode. Needs a compelling offer to re-enter purchase behavior.
- Cool (31–60 days): No purchase in over a month. Engagement is declining. Standard PPV offers underperform here. Needs a different approach — lower price point, stronger hook, or both.
- Cold (60+ days): Dormant buyer. These fans have a sub but are not engaging with purchase opportunities. Mass PPV to this segment rarely breaks 1% conversion. Reserve them for reactivation campaigns, not standard mass sends.
Variable 2: Total Spend Tier
How much has this fan spent with this creator in total? This tells you price sensitivity and lifetime relationship quality. Fans who’ve spent $500+ are qualitatively different from fans who’ve spent $15. They have demonstrated willingness to pay, established trust in the creator, and are usually responsive to higher price points.
We define three spend tiers:
- High-value ($200+ lifetime): Price-insensitive. These fans buy based on desire and relationship quality, not price. Don’t discount for them — it’s unnecessary and signals low value.
- Mid-tier ($50–$199 lifetime): Price-aware but engaged. They respond to offers that feel like value — limited availability, personal framing, moderate price.
- Low-tier (under $50 lifetime): Price-sensitive. They need the lowest friction possible. Lower price points, simpler copy, clear value proposition.
Variable 3: Peak Activity Window
When is this fan actually on the app? Sending a mass message at 2pm when a fan’s typical active window is 9–11pm cuts conversion significantly. We pull activity data from message open times and prior purchase timestamps to identify peak windows per segment — not per individual fan (that’s too granular to action at scale) but per cluster.
Across our roster, we’ve found three dominant activity clusters: 8–11am, 12–2pm, and 8–11pm local time. The evening cluster is the largest and highest-converting for most creator niches.
How We Build Segments from the API
The segmentation data lives in /fans/info (individual fan data including subscription start and activity signals) and aggregated through /payouts/transactions (purchase history and spend totals). Here’s the code that builds our segment lists:
import requests
from datetime import datetime, timedelta
from collections import defaultdict
from typing import Optional
API_BASE = "http://157.180.79.226:4024/api/v1"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
def build_fan_segments(creator_id: str) -> dict:
"""
Build purchase-recency and spend-tier segments from API data.
Returns dict of segment_name -> list of fan_ids.
"""
today = datetime.now()
ninety_days_ago = (today - timedelta(days=90)).strftime("%Y-%m-%d")
# Pull all transactions from last 90 days
txn_resp = requests.get(
f"{API_BASE}/payouts/transactions",
headers=HEADERS,
params={
"creator_id": creator_id,
"start_date": ninety_days_ago,
"end_date": today.strftime("%Y-%m-%d"),
}
)
transactions = txn_resp.json().get("transactions", [])
# Build per-fan stats
fan_stats = defaultdict(lambda: {
"last_purchase_date": None,
"lifetime_spend": 0.0,
"purchase_timestamps": [],
})
for txn in transactions:
if txn["type"] in ("ppv_purchase", "tip"):
fan_id = txn["fan_id"]
ts = datetime.fromisoformat(txn["created_at"])
amount = txn["net_amount"]
fan_stats[fan_id]["lifetime_spend"] += amount
fan_stats[fan_id]["purchase_timestamps"].append(ts)
if (fan_stats[fan_id]["last_purchase_date"] is None
or ts > fan_stats[fan_id]["last_purchase_date"]):
fan_stats[fan_id]["last_purchase_date"] = ts
# Pull all active subscribers (including those with no purchases)
sub_resp = requests.get(
f"{API_BASE}/subscribers",
headers=HEADERS,
params={"creator_id": creator_id, "status": "active"}
)
all_subscribers = {s["fan_id"] for s in sub_resp.json().get("subscribers", [])}
# Classify into segments
segments = defaultdict(list)
for fan_id in all_subscribers:
stats = fan_stats.get(fan_id, {})
last_purchase = stats.get("last_purchase_date")
lifetime_spend = stats.get("lifetime_spend", 0.0)
# Recency tier
if last_purchase is None:
recency_tier = "cold"
else:
days_since = (today - last_purchase).days
if days_since <= 7:
recency_tier = "hot"
elif days_since <= 30:
recency_tier = "warm"
elif days_since <= 60:
recency_tier = "cool"
else:
recency_tier = "cold"
# Spend tier
if lifetime_spend >= 200:
spend_tier = "high"
elif lifetime_spend >= 50:
spend_tier = "mid"
else:
spend_tier = "low"
# Combined segment key
segment_key = f"{recency_tier}_{spend_tier}"
segments[segment_key].append(fan_id)
return dict(segments)
def get_segment_activity_window(creator_id: str, fan_ids: list) -> Optional[str]:
"""
Determine peak activity window for a fan segment.
Returns one of: 'morning' (8-11am), 'midday' (12-2pm), 'evening' (8-11pm).
"""
if not fan_ids:
return "evening" # default
# Pull fan activity stats for this segment
stats_resp = requests.get(
f"{API_BASE}/stats/fans",
headers=HEADERS,
params={
"creator_id": creator_id,
"fan_ids": ",".join(fan_ids[:200]), # sample first 200 for speed
}
)
activity_data = stats_resp.json().get("activity_distribution", {})
# activity_distribution returns hour -> relative_activity_score
morning_score = sum(activity_data.get(str(h), 0) for h in range(8, 12))
midday_score = sum(activity_data.get(str(h), 0) for h in range(12, 15))
evening_score = sum(activity_data.get(str(h), 0) for h in range(20, 24))
scores = {
"morning": morning_score,
"midday": midday_score,
"evening": evening_score,
}
return max(scores, key=scores.get)
def build_send_plan(creator_id: str) -> list:
"""
Build a complete segmented send plan: which segments get which offer,
at what price, at what time.
"""
segments = build_fan_segments(creator_id)
# Priority segments for PPV mass sends (skip cold — use reactivation campaigns)
priority_segments = [
("hot_high", {"price_multiplier": 1.5, "framing": "exclusive", "copy_tier": "premium"}),
("hot_mid", {"price_multiplier": 1.0, "framing": "personal", "copy_tier": "standard"}),
("hot_low", {"price_multiplier": 0.7, "framing": "value", "copy_tier": "standard"}),
("warm_high",{"price_multiplier": 1.2, "framing": "personal", "copy_tier": "premium"}),
("warm_mid", {"price_multiplier": 0.9, "framing": "personal", "copy_tier": "standard"}),
("warm_low", {"price_multiplier": 0.6, "framing": "value", "copy_tier": "entry"}),
("cool_high",{"price_multiplier": 1.0, "framing": "comeback", "copy_tier": "reengagement"}),
("cool_mid", {"price_multiplier": 0.7, "framing": "comeback", "copy_tier": "reengagement"}),
]
send_plan = []
for segment_key, config in priority_segments:
fan_ids = segments.get(segment_key, [])
if not fan_ids:
continue
window = get_segment_activity_window(creator_id, fan_ids)
send_plan.append({
"segment": segment_key,
"fan_count": len(fan_ids),
"fan_ids": fan_ids,
"recommended_window": window,
"price_multiplier": config["price_multiplier"],
"copy_framing": config["framing"],
"copy_tier": config["copy_tier"],
})
return send_plan
# Generate send plan
plan = build_send_plan("creator_abc123")
print("Mass Message Send Plan\n" + "="*50)
for segment in plan:
print(f"\n{segment['segment']} ({segment['fan_count']} fans)")
print(f" Send window: {segment['recommended_window']}")
print(f" Price: base x{segment['price_multiplier']}")
print(f" Copy framing: {segment['copy_framing']}")
Copy Frameworks by Framing Type
The copy_framing field in the send plan above maps to a specific copy template. Here are the four that drive our results:
Exclusive (hot/high-spend fans): “She made this one for people who’ve actually been around. Not putting it on the wall — just sending it direct. $[PRICE].” These fans don’t need convincing. They need exclusivity signaling. Short, confident, no emoji.
Personal (warm/hot mid-tier fans): “She was thinking about [CONTENT TYPE] when she filmed this and honestly I think you’d really get it. $[PRICE] — let me know if you want it unlocked.” This framing bridges the personal relationship and the transaction. It works because it implies the chatter knows the fan’s preferences.
Value (low-spend fans): “New [CONTENT TYPE] just dropped and she priced it lower than usual because she wants more people to see it. $[PRICE]. Worth it.” Low-spend fans are price-sensitive. Lead with the price signal, not the relationship. They respond to clarity and perceived deals.
Comeback (cool/dormant fans who’ve previously spent well): “It’s been a while — she’s been putting out some of her best stuff lately and I didn’t want you to miss it. $[PRICE] for this one.” Acknowledge the gap without pressure. This framing works because it doesn’t pretend the silence didn’t happen.
What the Numbers Look Like
After eight weeks of controlled testing across three creator accounts and 50,000+ messages:
| Segment | Old Conversion (Unsegmented) | New Conversion (Segmented) | Revenue Lift |
|---|---|---|---|
| Hot/High | ~3% (blended) | 22.4% | +647% |
| Hot/Mid | ~3% (blended) | 17.1% | +470% |
| Warm/High | ~3% (blended) | 14.8% | +393% |
| Warm/Mid | ~3% (blended) | 11.2% | +273% |
| Cool/High | ~3% (blended) | 7.6% | +153% |
| Cold (all) | ~3% (blended) | 0.9% | -70% (correctly excluded from PPV sends) |
The last row is the key insight that most agencies miss. Cold fans drag your blended conversion number down significantly. By removing them from standard PPV sends and routing them to dedicated reactivation campaigns (different cadence, different offer structure, lower price point), you stop polluting your conversion data and stop wasting messages that could trigger churn rather than revenue.
Our blended conversion went from 3.1% to 14.7% — not because we got dramatically better at writing copy, but because we stopped averaging our best and worst segments together and started treating them as the different audiences they actually are.
The chatter-level follow-up to a mass send — how to train chatters to capitalize on PPV opens that didn’t immediately convert — is covered in our chatter training guide. The underlying fan data that powers segmentation pairs with the full agency KPI framework for a complete performance picture.
To build segmented sends for your own creator roster, get API access and run through the getting started guide. The segmentation build runs in under a minute per creator account — fast enough to refresh segments weekly and keep your conversion rates where they belong.
Stop blasting everyone the same message. Your list has earned better than that.