Use cases

Recover paywall abandonment with an iMessage offer in seconds

ZeroSettle lets a subscription iOS app text a user who just left the paywall a short, branded offer from the app's own iMessage number: three texts a few seconds apart, offer last, with a tapback on it. Each send normally completes in 2–5 seconds, so the whole offer goes out within seconds of the user leaving the app. Only users who opted in to texts during onboarding qualify, and ZeroSettle's docs set the pace at one offer per trigger and one trigger per user per week. On the $49-a-month Pro plan, one number can start offers with 5 new users a day; Growth allows 50 a day per number.

Key takeaways

  • What it is: a three-text offer from your app's own iMessage number, with your brand name and photo, that your backend sends through ZeroSettle's REST API when the app goes to the background on the paywall.
  • Speed: each send returns once the line has handed the text to Apple, normally in 2–5 seconds, so a three-text offer goes out within seconds of the user leaving the app.
  • Who gets it: users who opted in to texts during onboarding, for example with a "Text me my offers" toggle, with the time stored. A number collected only for login does not qualify.
  • Main limit: on Pro, 5 new contacts a day per number (Growth: 50 a day, 3 per rolling 15 minutes). Only the first text to each user counts; the rest of the offer, replies and later offers to that user do not.
  • Price: Pro is $49/month for one number; Growth, for several numbers working as one, is from $599/mo. There are no per-message fees.
  • What it does not do: reach users on Android (there is no SMS fallback), or make a link tappable before the user has replied or saved your number.

Who uses iMessage for paywall abandonment?

An iMessage offer after a paywall exit suits subscription iOS apps (fitness, language learning, calorie tracking, productivity) whose users meet the paywall in onboarding and leave without buying. ZeroSettle is an iMessage API: a real phone number on real iMessage, running on Apple hardware we operate for you, that your code texts customers from and hears back on through a bearer token and signed webhooks.

A user who swipes home from the paywall has seen the price but not said no. ZeroSettle's docs build the recipe around speed: the text has to arrive "before they have unlocked the next app".

For other retention moments, see trial-ending reminders, a "text the founder" cancel step, win-back texts and the use-case hub. Apps without text consent are not a fit.

How iMessage for paywall abandonment works, step by step

The app tells your backend the instant the user leaves the paywall; the backend sends three texts through ZeroSettle, offer last, and sends a link only after the user replies YES.

  1. Consent in onboarding: a phone field with a "Text me my offers" toggle. Store when the user switched it on.
  2. Arm the paywall only: the paywall view sets a flag on appear and clears it on disappear, so no other screen fires an offer; a second flag allows one ping per visit.
  3. Catch the exit: on UIApplication.willResignActiveNotification, posted "when the app is no longer active and loses focus" (Apple), the app pings your backend inside beginBackgroundTask, started before the first await. SwiftUI's scenePhase lands a run loop later.
  4. Check and queue: the backend skips users without consent, users with an offer in the last 7 days, and times outside 8 am to 9 pm in the user's time zone. It marks the offer sent, queues the work and answers 200 at once.
  5. Make the offer real: create the discount before the first text, as a web-checkout promo code or an App Store offer code. Apple's offer codes "provide Apple In-App Purchases at a discount or for free, for a specific duration".
  6. Three texts, offer last: each POST /api/imessage/send returns once the line has handed the text to Apple, normally in 2–5 seconds, which keeps the order and sets the cadence. Then POST /api/imessage/reactions puts an emphasize tapback on the offer.
  7. YES, then the link: the reply reaches your webhook as a signed message.received event about 1–2 seconds after the line receives it. The backend answers with a love tapback and the link, now tappable (see why links in a first iMessage aren't tappable).

When the first offer may not notify

Screen Unknown Senders (iOS 26) moves texts from unknown senders into a separate list without a notification, so a first offer to a user with it on may go unseen.

The exits that aren't exits

willResignActiveNotification also fires when the user isn't leaving: Apple says an app loses focus "when an overlay window pops up", and ZeroSettle's docs add a phone call, Control Center and a Face ID prompt. The once-per-visit flag caps that at one stray offer; waiting for didEnterBackgroundNotification instead costs an extra half second. Clear the paywall flag when the user taps your buy button too, so a system sheet mid-purchase can't text a buyer a discount.

Example conversation: paywall abandonment

Riley leaves a calorie-tracking app's paywall at 8:14 PM, inside the 8 am to 9 pm window. The first text names the app, as ZeroSettle's consent rules ask (see how to write a first iMessage customers reply to). The 30% code exists before the first text.

  1. Today 8:14 PM
  2. Calorie App: wait, it's Calorie App 👀
  3. Calorie App: we saved your 6-day streak
  4. Calorie App: want 30% off Pro to keep it going? Reply YES and I'll send it‼️
  5. Riley: YES❤️
  6. Calorie App: Here's your link, good for 24 hours: calorieapp.example/pro30
  7. Riley: done!
  8. Calorie App: 🔥 Streak saved. Welcome to Pro.
Illustrative example with a fictional app. Blue bubbles are sent from the app's number through the API; gray bubbles are Riley's. Both tapbacks come from the app's backend.

Setup: an app-background trigger and a three-text sequence

The setup has two parts, an app trigger that pings your backend and a backend that sends the texts and handles the YES, both from the Retention offers section of the ZeroSettle docs.

In the app

The docs' Swift class, PaywallExitOffer, covers steps 2 and 3: the paywall view arms it in .onAppear and disarms it in .onDisappear, and on exit it opens the background task and POSTs to your backend with the user's session. It prewarms the connection when the paywall appears, since a cold TLS handshake costs 300–800 ms. The app never calls ZeroSettle, so the token stays out of the binary.

On your backend

This FastAPI sketch sends the sequence and verifies X-ZeroSettle-Signature (HMAC-SHA256 over "<t>." + raw body, 300-second tolerance) before it reads a reply; verify() is the docs' own.

python
# paywall.py: run with  uvicorn paywall:app --port 8100
import hashlib, hmac, json, os, threading, time
import requests
from fastapi import BackgroundTasks, Depends, FastAPI, Request, Response
from yourapp import billing, current_user, offers, waking_hours  # your own code

BASE = "https://portal.zerosettle.io"
AUTH = {"Authorization": f"Bearer {os.environ['ZEROSETTLE_TOKEN']}"}
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
SCRIPT = ["wait, it's Calorie App 👀", "we saved your {streak}-day streak",
          "want 30% off Pro to keep it going? Reply YES and I'll send it"]
app = FastAPI()
pending = {}                      # E.164 handle -> offer (use your database)
line = threading.Lock()           # a line sends one message at a time

def verify(header, raw):
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t, v1 = int(parts["t"]), parts["v1"]
    except Exception:
        return False
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(SECRET.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

def zs(path, body):
    with line:
        return requests.post(f"{BASE}/api/imessage/{path}", json=body, headers=AUTH, timeout=90)

def send_offer(user):
    offer = billing.create_promo(user, percent_off=30, expires_in_hours=24)
    for text in SCRIPT:
        r = zs("send", {"to": user.phone, "text": text.format(streak=user.streak)})
        if r.status_code != 200:
            return                # a half-sent script beats a re-sent one
        last_id = r.json()["id"]
    pending[user.phone] = offer   # before the tapback, so a quick YES finds it
    zs("reactions", {"message_id": last_id, "reaction": "emphasize"})

@app.post("/left-paywall")
def left_paywall(tasks: BackgroundTasks, user=Depends(current_user)):
    if not user.sms_consent_at or offers.sent_within(user, days=7) or not waking_hours(user.tz):
        return {"ok": True}
    offers.mark_sent(user)        # before the first send, so a retried ping can't double-fire
    tasks.add_task(send_offer, user)
    return {"ok": True}

@app.post("/webhook")
async def webhook(request: Request, tasks: BackgroundTasks):
    raw = await request.body()
    if not verify(request.headers.get("X-ZeroSettle-Signature", ""), raw):
        return Response(status_code=401)
    ev = json.loads(raw)
    if ev["event"] != "message.received":
        return {"ok": True}       # your own sends echo back as message.sent
    m = ev["message"]
    if (m.get("text") or "").strip().rstrip(".!").upper() in ("YES", "Y") and m["handle"] in pending:
        tasks.add_task(claim, pending.pop(m["handle"]), m)
    return {"ok": True}

def claim(offer, m):
    zs("reactions", {"message_id": m["id"], "reaction": "love"})
    zs("send", {"to": m["handle"], "text": f"Here's your link, good for 24 hours: {offer.url}"})

Register the receiver with PUT /api/imessage/webhook and store the secret, shown once. The lock queues sends in your process, since a send that waits about 12 seconds at the line gets 429 line_busy. Popping the pending offer on the first YES means a webhook retry, which carries the same message.id, can't send the link twice. Test on a physical iPhone, since the Simulator has no iMessage delivery.

Limits and consent for paywall abandonment

The first text ZeroSettle delivers to each user counts against the new-contact limit, so on Pro one number can start 5 offers a day; on Growth, 50 a day and at most 3 in any 15 minutes. The rest of an offer, replies and later offers to the same user don't count.

Limits and rules that shape paywall offers on ZeroSettle (API docs, checked October 12, 2026)
RuleProGrowth
New contacts per number per day (3 AM to 3 AM Eastern)550
New contacts per number per rolling 15 minutesNo separate cap (the daily 5 binds first)3
Unanswered messages to one contact2 until they reply (texts within 5 minutes of the first count as one)
Offers per user (docs' rule of thumb; your backend enforces it)One trigger per week
Promotional quiet hours in the recipient's time zone (your backend enforces them)Before 8 am and after 9 pm

The limit math for a paywall

Take an app where 30 opted-in users who have never had a text from it leave the paywall in one day, 12 of them between 7:00 and 7:15 PM.

  • The evening rush on Pro: 5 of the 12 get their offer right away; the other 7 get 429 new_contact_limit with retry_after_s. A late offer is a stale one, so the sketch drops it; if you retry, set a cutoff.
  • The day on Pro: 5 users get an offer. The other 25 would wait for the 3 AM Eastern reset, so in practice they get none.
  • On Growth: each number takes only 3 new contacts per 15 minutes, and a send is refused only when every number is full, so a three-number account reaches 9 of the 12 in the rush. A three-text offer counts as one unanswered message, so a third weekly offer to someone who never replied returns 403 awaiting_reply, on Pro and Growth alike.

Consent comes first

ZeroSettle's docs warn that US marketing texts generally need written consent and that TCPA damages can run $500 to $1,500 per message (see the TCPA consent guide). ZeroSettle enforces STOP on every number on your account: a text with the word STOP, or a clear request like "take me off your list", opts the user out, later sends to them return 403 recipient_opted_out, and only START opts them back in. This isn't legal advice; have counsel review your consent flow.

When ZeroSettle is not a fit for paywall abandonment

ZeroSettle is the wrong tool for paywall offers when:

  • You don't collect explicit text consent. Consent means the user gave you their number for these texts, and you can show the record. A number collected for SMS login isn't consent, and ZeroSettle suspends lines used for unsolicited messaging.
  • More than 5 opted-in users a day leave the paywall and you'll stay on one Pro number. The rest get nothing that day.
  • You need to reach Android users too. There is no SMS fallback, so Android users and numbers not on iMessage get nothing.
  • You can't make the offer real before you send it. Without a web checkout, a promo code or an offer code ready to redeem, "30% off" is only copy.
  • You need to know who read the offer. The API reports message.sent and message.failed, not read status.

How much does iMessage for paywall abandonment cost?

Paywall offers cost the plan price: ZeroSettle has no per-message fees, and messages are unlimited (counted).

ZeroSettle plans for paywall offers (zerosettle.io, checked October 12, 2026)
PlanPriceFits
Pro$49/month, no setup feeOne number to start, 5 new contacts a day, unlimited messages with people who message you first, self-serve
GrowthFrom $599/mo3+ numbers working as one, 50 new contacts a day per number; book a demo

Both plans carry a 30-day money-back guarantee on the subscription. AI credits don't apply, because your backend writes the offer. See ZeroSettle's plans, or get a number.

Frequently asked questions

Will Apple reject my app for texting offers?

ZeroSettle's FAQ answers no, citing App Store guideline 3.1.3: "Developers can send communications outside of the app to their user base about purchasing methods other than in-app purchase." Point the link at a purchase path your app may use, such as an offer code's redemption URL, and read the current guideline yourself.

Will the offer show my app's name or a bare number?

Your name and photo share automatically with the first text; the user sees "<name> shared a name and photo with you" and can save it. ZeroSettle's homepage says customers see "Maybe: Your Company" and your photo even if they've never saved your number, though their contact settings can change what shows.

Why three texts instead of one?

ZeroSettle's docs: "Three texts a few seconds apart read like a person typing; one paragraph reads like a campaign." The docs also advise against more than three, because a long script reads as spam.

Can I send the checkout link right away?

It goes out, but not as a link they can tap: until the user replies or saves your number, iPhone shows it as plain text, and ZeroSettle's response adds warning.code: "link_not_tappable". Ask for YES first.

Sources

  1. ZeroSettle iMessage API docs: "Retention offers", "Your iMessage number", "Send a message", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 8, 2026.
  2. ZeroSettle homepage: Plans and the FAQ "Can texting people who leave my paywall get my app banned?". Checked October 8, 2026.
  3. App Review Guidelines, guideline 3.1.3, Apple Developer. Checked October 8, 2026.
  4. willResignActiveNotification, UIKit documentation, Apple Developer. Checked October 8, 2026.
  5. beginBackgroundTask(withName:expirationHandler:), UIKit documentation, Apple Developer. Checked October 8, 2026.
  6. Supporting offer codes in your app, StoreKit documentation, Apple Developer. Checked October 8, 2026.

Written by the ZeroSettle team. ZeroSettle is an iMessage API: a real phone number on real iMessage, running on Apple hardware we operate for you, that your code texts customers from and hears back on through a bearer token and signed webhooks.

Product facts on this page come from the ZeroSettle iMessage API docs and the zerosettle.io homepage as of October 12, 2026. Outside facts come from the pages listed under Sources, checked October 8, 2026. Calorie App and Riley are fictional, and nothing here is legal advice.

Text your customers from your own iMessage number

ZeroSettle gives your business its own phone number on real iMessage, running on Apple hardware we operate for you in a server facility. Your code sends with one API call and hears every reply on a signed webhook.

Pro is $49 a month with a 30-day money-back guarantee. Growth, for several numbers working as one, is from $599/mo.