Trial-ending and renewal reminders over iMessage
ZeroSettle lets a subscription app or SaaS send trial-ending and renewal reminders over iMessage: text opted-in users the day before a trial ends ("Reply YES to keep it at $4.99/mo") and seven days before an annual renewal ("Reply KEEP, PAUSE or CANCEL"), then map each reply to your billing API from the webhook. A YES gets a checkout link, which is tappable because the user has replied. Keep the email notice too: a text can add to the notice that automatic-renewal laws require, but it shouldn't replace it.
Key takeaways
- What it is: billing reminders sent from your app's own iMessage number by your server through ZeroSettle's REST API, with each reply arriving on a signed webhook.
- Who it fits: subscription apps, SaaS products and memberships with free trials or annual plans, whose users opted in to account texts.
- 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.
- Main limit: 5 new contacts a day per number on Pro (50 on Growth). A user's first reminder counts once; replies and later reminders don't.
- What it does not do: reach Android users (there is no SMS fallback) or stand in for the renewal notice the law requires.
Who uses iMessage for trial and renewal reminders?
Trial and renewal reminders on ZeroSettle suit subscription apps, SaaS products and memberships with free trials or annual plans, whose users gave a number for account texts. 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.
The problem has two sides. A trial-ending email is easy to miss, so a no-card trial lapses by default. A card-on-file trial or annual plan renews silently, and the surprise comes back as refund requests, chargebacks and angry reviews. A one-word reply addresses both, because the reply is the billing action.
Related billing moments: paywall exits, failed payments and skips before a box ships; all are on the use cases hub.
How iMessage trial and renewal reminders work, step by step
Two billing events share one pattern: your billing system says a reminder is due, your server sends one link-free text, and the reply on the webhook becomes a billing call.
- Consent at signup: a checkbox next to the mobile number field says you'll text about the account, including the trial's end and plan offers. Store it with a timestamp.
- Trial trigger: Stripe sends
customer.subscription.trial_will_endthree days before a trial ends by default (at once for shorter trials), so use it to schedule a text for 24 hours before the trial ends, or run your own job. - Send: your server calls
POST https://portal.zerosettle.io/api/imessage/sendwith "Your Pro trial ends tomorrow. Reply YES to keep it at $4.99/mo, or tell us what's not working." No link yet: the recipient's iPhone shows a link from an unknown sender as plain text, and the API flags one withwarning.code: "link_not_tappable". - Reply: a signed
message.receivedevent reaches your webhook about 1–2 seconds after the line receives it. A YES creates a checkout session and sends the link, tappable now. Any other answer goes to a person in the ZeroSettle console, or to your model, which offers a cheaper plan or a pause. - Annual renewal, seven days out: a nightly job (or Stripe's
invoice.upcomingevent) sends the date and amount with "Reply KEEP, PAUSE or CANCEL", and the webhook maps each answer to your billing API. - One follow-up at most, then stop. The email notice goes out on its own schedule either way.
Example conversation: trial and renewal reminders
This trial reminder ends in a cheaper plan instead of a lapse, as the app sees it in the ZeroSettle console.
- Wed 6:00 PM
- Business: Hi Ana, your Glotfinch Pro trial ends tomorrow. Reply YES to keep Pro at $4.99/mo, or tell us what's not working.
- Customer: honestly i only use it on weekends
- Business: Totally fair. Want the weekend plan instead? $1.99/mo, same lessons.
- Customer: oh yes
- Business: Here you go: glotfinch.example/w/ana👍
Ana's first answer isn't a YES, so the app's model offers a plan that already exists in billing. Her 👍 arrives as reaction.received.
Setup: billing-event triggers and reply mapping
Setup is one send per billing event plus a webhook receiver that turns a one-word reply into a billing call. The receiver below verifies X-ZeroSettle-Signature, answers within 10 seconds and deduplicates retries (see how ZeroSettle webhooks are signed and retried). billing, db and hand_off stand for your own code, and the Stripe calls in the comments are examples.
# reminders.py: run with uvicorn reminders:app --port 8100
import hashlib, hmac, json, os, re, time
import requests
from fastapi import BackgroundTasks, FastAPI, Request, Response
from yourapp import billing, db, hand_off # your own code: Stripe calls, reminder rows, hand-off
BASE = os.environ.get("ZEROSETTLE_BASE_URL", "https://portal.zerosettle.io")
TOKEN = os.environ["ZEROSETTLE_TOKEN"]
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
app = FastAPI()
seen: set[tuple[int, str]] = set() # use your database in production
def verify(header: str, raw: bytes) -> bool:
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 send(to: str, text: str) -> None:
requests.post(f"{BASE}/api/imessage/send", json={"to": to, "text": text},
headers={"Authorization": f"Bearer {TOKEN}"}, timeout=90)
def handle(m: dict) -> None:
r = db.open_reminder(m["handle"]) # the reminder this number got, or None
text = (m.get("text") or "").strip()
if r is None:
return
if re.search(r"\b(stop|unsubscribe)\b", text, re.I):
return hand_off(r, text) # opted out: confirm anything by email
word = text.upper().strip(" .!")
if r.kind == "trial" and word in ("YES", "Y"):
send(m["handle"], f"Here you go: {billing.checkout_url(r)}") # Checkout Session url
elif r.kind == "renewal" and word == "KEEP":
send(m["handle"], f"You're set. Pro renews on {r.renews_on}.")
elif r.kind == "renewal" and word == "PAUSE":
billing.pause(r) # Stripe: pause_collection + resumes_at
send(m["handle"], f"Paused. No charge on {r.renews_on}.")
elif word == "CANCEL":
billing.cancel(r) # Stripe: cancel_at_period_end=true
send(m["handle"], f"Done. Your plan ends {r.renews_on}, with no further charges.")
else:
hand_off(r, text) # a person in the console, or your model
@app.post("/webhook")
async def webhook(req: Request, tasks: BackgroundTasks):
raw = await req.body()
if not verify(req.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["id"], ev["event"]) in seen:
return {"ok": True}
seen.add((m["id"], ev["event"]))
tasks.add_task(handle, m)
return {"ok": True}
Register the receiver with PUT /api/imessage/webhook and store the signing secret, shown only once. The ZeroSettle docs on retention offers use the same reply-to-billing loop.
Limits and consent for trial and renewal reminders
Each user's first reminder counts once against ZeroSettle's new-contact limit (Pro: 5 a day per number); replies and every later text in that conversation don't. Consent is the stricter rule, and the ZeroSettle terms require opt-in before any message: text only users who opted in at signup, keep that record, and name your app in the first text.
| Limit | Pro | Growth |
|---|---|---|
| New contacts per day (3 AM to 3 AM Eastern) | 5 | 50 |
| New contacts per rolling 15 minutes | Not stated; the daily 5 is the limit | 3 |
| Unanswered messages to one contact | 2, then 403 awaiting_reply | |
| Replies and ongoing conversations | Unlimited | Unlimited |
The limit math for one day of trial endings
Say your job sends tomorrow's 30 trial reminders in one batch, and 8 of those users have texted your number before, leaving 22 new contacts.
- Pro: 5 reminders go out; the other 17 get
429 new_contact_limitand wait for the 3 AM Eastern reset, and at 5 a day most would land after their trials end. Pro fits when no more than about 5 users a day get their first reminder; at this volume, Growth does. - Growth: each number takes 3 new contacts per 15 minutes, and a send is refused only when every number is full. Three numbers send 9 every 15 minutes and finish all 22 in about 30 minutes; one number alone would need about 1 hour 45 minutes. Each number allows 50 a day.
- Renewals cost nothing against the limit for anyone the line has texted before (unless you deleted that conversation). On every plan, a user who ignored both trial texts is already at 2 unanswered, so the renewal text returns
403 awaiting_reply: that user gets the email only.
Consent, STOP and the renewal notice
- Transactional vs marketing: as a planning default, treat a renewal notice (date, amount, how to cancel) as account information. A plan offer like the weekend plan is marketing, and a no-card trial's "Reply YES to keep it" also asks for a purchase, so treat it the same way. ZeroSettle's docs say marketing texts need written consent, which the FCC's TCPA rule defines as a signed agreement naming the number (an electronic signature counts). Send nothing promotional before 8 am or after 9 pm in the user's time zone (more in the TCPA consent guide). This isn't legal advice; have counsel classify your texts.
- CANCEL is fine; "stop" is an opt-out. A text with the word STOP in it, like "stop charging me", or an opt-out phrase such as "unsubscribe" or "remove me", opts the user out of texts from every number on your account; only START opts them back in. The plan doesn't change, and later sends return
403 recipient_opted_out. Never write "Reply STOP to cancel." - Keep the email notice. California's automatic renewal law, for example, requires notice 15 to 45 days before a contract with an initial term of a year or more renews, and 3 to 21 days before a free trial longer than 31 days ends. Stripe's optional trial reminder email, meant to help meet card network rules, goes out 7 days before a trial ends. The text is an extra reminder on top. Check which notice rules apply to you with counsel; this isn't legal advice.
When ZeroSettle is not a fit for trial and renewal reminders
ZeroSettle is the wrong tool for billing reminders in these cases:
- You'd replace the legally required renewal notice with a text instead of sending both.
- Users didn't opt in to account texts, for example because you collected numbers only for sign-in codes.
- Many users are on Android. There is no SMS fallback, so email is their only reminder.
- You'd use the thread to make cancelling harder. California's law allows a save offer during cancellation only if the consumer can still cancel right away.
- You routinely need more than 5 first-time reminders a day on one Pro number. Growth allows 50 per number.
How much do iMessage trial and renewal reminders cost?
Trial and renewal reminders cost the ZeroSettle plan price: there are no per-message fees and no cap on messages; only first texts to new contacts and unanswered follow-ups are limited.
| Plan | Price | Fits |
|---|---|---|
| Pro | $49/month | One number, 5 new contacts a day, unlimited messages with people who message you first, no setup fee, self-serve |
| Growth | From $599/mo | Several numbers working as one, 50 new contacts a day per number |
Both carry a 30-day money-back guarantee; ZeroSettle itself has no free trial. See ZeroSettle pricing, get a Pro number, or book a Growth demo.
Frequently asked questions
Does a card-on-file trial need a different text from a no-card trial?
Yes. A no-card trial lapses unless the user acts, so ask for YES and send a checkout link. A card-on-file trial converts on its own, so state the date and charge and offer CANCEL.
Can I try to keep someone who replies CANCEL?
Cancel first. The confirmation can mention a pause once ("Reply PAUSE if you'd rather take a break"), but the chat should never stand between the user and cancelling.
What happens to trial users on Android?
ZeroSettle can't reach them. You can check whether a number is on iMessage before texting it, and the check sends nothing; users who aren't on iMessage get the email alone.
Can the hosted AI agent answer the replies instead of my server?
Not on a number your billing code sends from. A line group set to Managed by AI Agent (Beta) answers inbound texts itself and refuses your own sends with 403 managed_by_zerosettle, so reminders and checkout links need a line group Managed by API, with your own model on the webhook.
Sources
- ZeroSettle iMessage API docs: "More than one number", "Send a message" (links in a first message), "Retention offers", "AI agent", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 12, 2026.
- ZeroSettle iMessage API Terms of Service, section 13 (opt-in before any message). Last modified September 28, 2026.
- Configure trial offers on subscriptions, Stripe Docs (the
customer.subscription.trial_will_endevent). Checked October 8, 2026. - Manage compliance requirements for trials and promotions, Stripe Docs (the 7-day trial reminder email and the
invoice.upcomingevent). Checked October 8, 2026. - Create a Checkout Session, Stripe API reference. Checked October 8, 2026.
- Pause payment collection and Cancel subscriptions, Stripe Docs. Checked October 8, 2026.
- California Business and Professions Code § 17602, subdivisions (b) and (e), statute text on public.law. Checked October 8, 2026.
- 47 CFR § 64.1200, Delivery restrictions, paragraphs (a)(2) and (f)(9), the FCC's TCPA rule (Legal Information Institute, Cornell Law School). Checked October 8, 2026.